mirror of
https://github.com/alibaba/canal.git
synced 2026-11-04 00:30:15 +08:00
Compare commits
479 Commits
canal-1.1.
...
canal-1.1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b12c7098 | ||
|
|
2c320e8618 | ||
|
|
d76d25df6c | ||
|
|
b95c0d03cf | ||
|
|
08af40221f | ||
|
|
d13c1eb17c | ||
|
|
82ed9db8ae | ||
|
|
d3195ac973 | ||
|
|
15e29d276a | ||
|
|
d715ff64b4 | ||
|
|
f63ce4ffe6 | ||
|
|
a7f9a779ad | ||
|
|
39aeeb8c58 | ||
|
|
9826948ebb | ||
|
|
d6ef66a281 | ||
|
|
97640b717b | ||
|
|
20e7becfe9 | ||
|
|
99b34ff632 | ||
|
|
739963acd0 | ||
|
|
0ec4699149 | ||
|
|
c101ae2280 | ||
|
|
a137deeaff | ||
|
|
c5be5b7a8a | ||
|
|
95bed6bcf6 | ||
|
|
661d119265 | ||
|
|
08167c95c7 | ||
|
|
37c5168a7d | ||
|
|
579fa72ae2 | ||
|
|
844c019433 | ||
|
|
c94e3a0024 | ||
|
|
f6ca489984 | ||
|
|
b2b69307f9 | ||
|
|
531b535228 | ||
|
|
99cccd2113 | ||
|
|
38d8c3d462 | ||
|
|
024ffc2aa2 | ||
|
|
a81d2710ff | ||
|
|
4bad0e88f8 | ||
|
|
0181dfaa42 | ||
|
|
0411d99e7b | ||
|
|
8fcdd7f798 | ||
|
|
b0f4e4ee78 | ||
|
|
d448455ebf | ||
|
|
e07fdb6426 | ||
|
|
38336b249e | ||
|
|
e7b81cd83a | ||
|
|
2810e7e815 | ||
|
|
024986a195 | ||
|
|
9b2ad0eafd | ||
|
|
03d46f150f | ||
|
|
b488ca02d8 | ||
|
|
17a6ba9c9c | ||
|
|
3b618740d4 | ||
|
|
06bfcda275 | ||
|
|
5821c1f606 | ||
|
|
a2360efff9 | ||
|
|
51c469aa6f | ||
|
|
6b3bf31404 | ||
|
|
941ed7f60d | ||
|
|
9db2bd291b | ||
|
|
95b26c4d1d | ||
|
|
b114e0cb15 | ||
|
|
5c96bf282e | ||
|
|
6d280d1048 | ||
|
|
cf4db1625d | ||
|
|
df98fec945 | ||
|
|
e03705e211 | ||
|
|
4df60a0fb5 | ||
|
|
078673c6a9 | ||
|
|
4bd91a1079 | ||
|
|
05ad671cc1 | ||
|
|
43045bc672 | ||
|
|
971c9a88c2 | ||
|
|
7b6e482827 | ||
|
|
ad39559f8c | ||
|
|
2707c12570 | ||
|
|
7dab5ac6c2 | ||
|
|
9d4b61f083 | ||
|
|
a34f9ae97b | ||
|
|
64ca62b986 | ||
|
|
14c1af4c80 | ||
|
|
7436d9f06f | ||
|
|
c4599df1c2 | ||
|
|
7abf50f233 | ||
|
|
1781381187 | ||
|
|
5d4210bd22 | ||
|
|
9888fae559 | ||
|
|
81ab7e9783 | ||
|
|
b891afa50e | ||
|
|
8187d7c5df | ||
|
|
e7f069850f | ||
|
|
d377a92a5d | ||
|
|
f664af1af4 | ||
|
|
5e2eabb3c2 | ||
|
|
f2368e5469 | ||
|
|
fd4c8db92b | ||
|
|
f21abc13af | ||
|
|
ae474d67ad | ||
|
|
0c4711ab62 | ||
|
|
072420c32b | ||
|
|
1473a4a6b5 | ||
|
|
0fff514876 | ||
|
|
4017de5102 | ||
|
|
fcc1fc78ae | ||
|
|
17efc2ada0 | ||
|
|
a68bd28852 | ||
|
|
1de8b27da1 | ||
|
|
01dc8ce2b8 | ||
|
|
f7e490f7a0 | ||
|
|
8cadd585c6 | ||
|
|
57c3226e56 | ||
|
|
79a3961da1 | ||
|
|
62338a945e | ||
|
|
e07db7b178 | ||
|
|
05cc627b5d | ||
|
|
44beaf1916 | ||
|
|
476244b26d | ||
|
|
cfdf509f9a | ||
|
|
c14ed724ff | ||
|
|
5a4cfeab46 | ||
|
|
947cf130ab | ||
|
|
0ef21e4155 | ||
|
|
83b760494d | ||
|
|
aba8df699c | ||
|
|
0c5f5c128f | ||
|
|
ad62275cd7 | ||
|
|
f6d03bcb37 | ||
|
|
5284aa9793 | ||
|
|
10276e254a | ||
|
|
597a08ac37 | ||
|
|
8211ef35e6 | ||
|
|
a20960834b | ||
|
|
7b6b332463 | ||
|
|
43f869599d | ||
|
|
e3d05f6201 | ||
|
|
032301102c | ||
|
|
bb64d84298 | ||
|
|
ccd39e9d62 | ||
|
|
32031da9db | ||
|
|
9a2e890f57 | ||
|
|
8f76b33214 | ||
|
|
a7467539c7 | ||
|
|
db5495d158 | ||
|
|
b04d948064 | ||
|
|
5f8eb4d261 | ||
|
|
197ca67c8b | ||
|
|
851637611f | ||
|
|
4ebf17f57b | ||
|
|
2f214ce3a6 | ||
|
|
8d4fbb395b | ||
|
|
d935c39648 | ||
|
|
6e0e3fbef1 | ||
|
|
cbe88af2e3 | ||
|
|
ba9d9dea6d | ||
|
|
2d88d803ee | ||
|
|
ff1fef1eb6 | ||
|
|
4beb1e6855 | ||
|
|
d2bbe9b061 | ||
|
|
171bedf488 | ||
|
|
e387cfdee5 | ||
|
|
47d7914304 | ||
|
|
e4177ab0ce | ||
|
|
739f3b4fa1 | ||
|
|
60441d8b31 | ||
|
|
ca41be8412 | ||
|
|
18b304f99d | ||
|
|
7f9dce4b77 | ||
|
|
f26adff7c7 | ||
|
|
77646c0185 | ||
|
|
7e4912cd2e | ||
|
|
70a4f0363d | ||
|
|
725960d46e | ||
|
|
b00d347eb0 | ||
|
|
92666b41e8 | ||
|
|
f1753e79f9 | ||
|
|
f1a998b2d5 | ||
|
|
03e6662ce5 | ||
|
|
b845a67666 | ||
|
|
690222bde8 | ||
|
|
63ad80c8c4 | ||
|
|
de501c85a1 | ||
|
|
670b7830a5 | ||
|
|
fdf150bd24 | ||
|
|
c66b6a0f0f | ||
|
|
a91aebafc2 | ||
|
|
35fcd56822 | ||
|
|
a63e982115 | ||
|
|
4bdf3d766f | ||
|
|
779ebad890 | ||
|
|
b348260288 | ||
|
|
3011c3292b | ||
|
|
94b8fdbbf4 | ||
|
|
1dd9934f9c | ||
|
|
3dec7c50ce | ||
|
|
ecd9ababa1 | ||
|
|
b0eaa156cc | ||
|
|
a70178afef | ||
|
|
114c3bd020 | ||
|
|
f9e19d46c4 | ||
|
|
661752096c | ||
|
|
100b43a5db | ||
|
|
5e116de264 | ||
|
|
a1091368fb | ||
|
|
71b7c0b868 | ||
|
|
7c79714528 | ||
|
|
8d84521767 | ||
|
|
14ba358525 | ||
|
|
7ea1468a8d | ||
|
|
f72ac9a3bb | ||
|
|
f100aeda39 | ||
|
|
953a0d0583 | ||
|
|
2afe7debda | ||
|
|
d8737a5228 | ||
|
|
150a13824b | ||
|
|
830e149d0e | ||
|
|
d9e259dde4 | ||
|
|
9e6adff942 | ||
|
|
82a78546ab | ||
|
|
19c35c0b08 | ||
|
|
ade2a54369 | ||
|
|
dffbc437a6 | ||
|
|
b5697a366a | ||
|
|
0469d28c05 | ||
|
|
d5f44dd76f | ||
|
|
354d64069f | ||
|
|
3465866278 | ||
|
|
840148fbf6 | ||
|
|
34b5f9c927 | ||
|
|
75ee65f9e8 | ||
|
|
035ef26542 | ||
|
|
4ef45b9a1d | ||
|
|
990fa9498a | ||
|
|
276a2b988a | ||
|
|
ef72cddfe8 | ||
|
|
d8d3246e30 | ||
|
|
bba800eb96 | ||
|
|
ff71bf515a | ||
|
|
6ef48591a1 | ||
|
|
a7526dbf4a | ||
|
|
51d94efc9c | ||
|
|
833cc55e26 | ||
|
|
689366248d | ||
|
|
3acdffb297 | ||
|
|
fbf249580f | ||
|
|
d2e3e19831 | ||
|
|
0523b68c26 | ||
|
|
995a33a645 | ||
|
|
5c4b1f6e63 | ||
|
|
ba5865683f | ||
|
|
89a4943349 | ||
|
|
35e0e882c1 | ||
|
|
0197482618 | ||
|
|
72bad4d54c | ||
|
|
48de904ee1 | ||
|
|
f6a931d8c6 | ||
|
|
277a273ebc | ||
|
|
bd5110b951 | ||
|
|
aeecf87490 | ||
|
|
eac3f5ce5f | ||
|
|
706ee88914 | ||
|
|
7c63e81e01 | ||
|
|
99a1123e01 | ||
|
|
24d898d87a | ||
|
|
5650b56bed | ||
|
|
547cbf82c8 | ||
|
|
03ee9c7ecf | ||
|
|
ac738dae05 | ||
|
|
dda8081984 | ||
|
|
f0fc37c43b | ||
|
|
5a99de1fa9 | ||
|
|
ce956c980d | ||
|
|
c045dc244e | ||
|
|
1b11aa1d21 | ||
|
|
dac642f744 | ||
|
|
19e164b599 | ||
|
|
339ecdd817 | ||
|
|
5fe7ae1613 | ||
|
|
65f2695963 | ||
|
|
cf5e42442b | ||
|
|
e7b678d5e0 | ||
|
|
f04447837c | ||
|
|
b7d2ad2e18 | ||
|
|
f1710f1c7f | ||
|
|
37094447b1 | ||
|
|
b97d377d83 | ||
|
|
46b6528273 | ||
|
|
6c3b3acde0 | ||
|
|
91b036a4fc | ||
|
|
67cd0e5fca | ||
|
|
9dfa5da7bc | ||
|
|
9bda60bf89 | ||
|
|
9aaf20fb5e | ||
|
|
8496ceaf15 | ||
|
|
cf23e0ffea | ||
|
|
4e9e3f5abe | ||
|
|
cbab3fb561 | ||
|
|
f7bf4ac6ff | ||
|
|
df4c66b9e2 | ||
|
|
a6ee87281a | ||
|
|
90dab6831b | ||
|
|
839d5415fc | ||
|
|
29f03bf398 | ||
|
|
ec235f3efd | ||
|
|
3b25b3b342 | ||
|
|
aa89453bf6 | ||
|
|
cc97f3b331 | ||
|
|
89ec3037d4 | ||
|
|
e711f1a091 | ||
|
|
ad18e0335d | ||
|
|
67cc73b8e2 | ||
|
|
638952fad1 | ||
|
|
56ec66f90e | ||
|
|
c0bbe530f1 | ||
|
|
4bfc476e08 | ||
|
|
5d71d50d4e | ||
|
|
26700f3005 | ||
|
|
a7be9c75f9 | ||
|
|
b6f1e0d13c | ||
|
|
50630cc934 | ||
|
|
43db5bec97 | ||
|
|
affd598470 | ||
|
|
178bea07a8 | ||
|
|
fcdf95b5ec | ||
|
|
991418ce6e | ||
|
|
d34bee3359 | ||
|
|
0a3c1645e8 | ||
|
|
9dc4d78b3b | ||
|
|
bc056288d4 | ||
|
|
8aea31232b | ||
|
|
fc6c871382 | ||
|
|
14c0e4535a | ||
|
|
7ee7ba0e05 | ||
|
|
7992b883d2 | ||
|
|
5bc0c3f06f | ||
|
|
b457ecd387 | ||
|
|
997646b2db | ||
|
|
915849b83a | ||
|
|
129118b483 | ||
|
|
aa68408c53 | ||
|
|
d396a0b65c | ||
|
|
827b872cf8 | ||
|
|
6f3cae5ddf | ||
|
|
31bf92ec8e | ||
|
|
68d8aee6c3 | ||
|
|
c376f0afd2 | ||
|
|
60e5554125 | ||
|
|
241c43a894 | ||
|
|
fecc112196 | ||
|
|
dd04577994 | ||
|
|
51a299259b | ||
|
|
0a0a39880c | ||
|
|
5ad9cf3f16 | ||
|
|
d29fe5b04e | ||
|
|
20062d861d | ||
|
|
73de8b32a9 | ||
|
|
8af1301062 | ||
|
|
8c3ade2d50 | ||
|
|
752a3cd66a | ||
|
|
154ba422f2 | ||
|
|
03a9522191 | ||
|
|
c4947cad3c | ||
|
|
3f8cdafea3 | ||
|
|
fcaf8e88f8 | ||
|
|
e7ab25f819 | ||
|
|
b45a27e231 | ||
|
|
2e5b7b89af | ||
|
|
66912d3d05 | ||
|
|
ec1ac6d11c | ||
|
|
7cca8857ff | ||
|
|
e7086415f8 | ||
|
|
4a27d81da1 | ||
|
|
739425e99c | ||
|
|
cc1574d913 | ||
|
|
d7ab6e2dc1 | ||
|
|
808bedf984 | ||
|
|
357eb885b2 | ||
|
|
417e04f655 | ||
|
|
2d41bb9960 | ||
|
|
7d7c82ce91 | ||
|
|
0c3f7a806e | ||
|
|
2a38bca34b | ||
|
|
c5bd42bbf1 | ||
|
|
8c1f15a49e | ||
|
|
f9778a0aff | ||
|
|
2ac84b4320 | ||
|
|
0677443ccf | ||
|
|
2c1f3e649a | ||
|
|
4faf5428b1 | ||
|
|
b71472661b | ||
|
|
6fa111866f | ||
|
|
f7ea8df89e | ||
|
|
041225c3f2 | ||
|
|
a8d708ea1b | ||
|
|
5ae7112192 | ||
|
|
2278340c7b | ||
|
|
87abaee8a7 | ||
|
|
8f2e2b9ccd | ||
|
|
d2ecb0aad9 | ||
|
|
1126d26574 | ||
|
|
b71e299161 | ||
|
|
572ea9f27a | ||
|
|
bc298bedd5 | ||
|
|
202557630c | ||
|
|
c27d5fa162 | ||
|
|
3090557eeb | ||
|
|
6de9f7efa7 | ||
|
|
f2b8d000f7 | ||
|
|
8ee546d34d | ||
|
|
5b94c69d10 | ||
|
|
8ae192f0a9 | ||
|
|
2e1a1c31e6 | ||
|
|
238da873b2 | ||
|
|
edb5f294ef | ||
|
|
5a76e2aa58 | ||
|
|
1e20cac0da | ||
|
|
d8e411462e | ||
|
|
59e65ed4a6 | ||
|
|
4319b5e888 | ||
|
|
0d9695c3a5 | ||
|
|
359aa1a074 | ||
|
|
e608f09b53 | ||
|
|
beed10901d | ||
|
|
dab6fa5aa1 | ||
|
|
30d20f824b | ||
|
|
80cf533b05 | ||
|
|
6ff57f53f8 | ||
|
|
650446de5b | ||
|
|
347601b9fe | ||
|
|
5c8a5998d9 | ||
|
|
391e040c6a | ||
|
|
8597d9e926 | ||
|
|
a895ce666e | ||
|
|
9d113dbc13 | ||
|
|
5cc2f8f9f7 | ||
|
|
5b9ada1f8c | ||
|
|
369059fa39 | ||
|
|
9473f4e8fd | ||
|
|
685053599f | ||
|
|
a9727cf267 | ||
|
|
bc399ba9f9 | ||
|
|
81f403a9f2 | ||
|
|
332f759f96 | ||
|
|
5d5aafb7cd | ||
|
|
f42f7cfe72 | ||
|
|
96b9484f70 | ||
|
|
3bb408194d | ||
|
|
865ba94605 | ||
|
|
bb883cd228 | ||
|
|
d3e1ddd234 | ||
|
|
b9c4884af1 | ||
|
|
a6b16eedfa | ||
|
|
9d04a82c42 | ||
|
|
c9ad18894d | ||
|
|
b2b44555ca | ||
|
|
14e094b522 | ||
|
|
0dea4b7944 | ||
|
|
94a3cfa9de | ||
|
|
ed1feb9502 | ||
|
|
a4856b9a73 | ||
|
|
ff3ccde62a | ||
|
|
2a2977eca7 | ||
|
|
1d9e2ccedd | ||
|
|
dd8243a56d | ||
|
|
2c9acb655d | ||
|
|
e1191b665e | ||
|
|
e4e23c9ecd | ||
|
|
4ce2b94241 | ||
|
|
11ac253c46 | ||
|
|
1d2e85672c | ||
|
|
82b8b56aa3 | ||
|
|
4e18d7800a | ||
|
|
7efd067bd5 | ||
|
|
4c7129115c | ||
|
|
0f14ed926b | ||
|
|
be5f1546ca | ||
|
|
f3160bf6cb | ||
|
|
fa79db65b7 | ||
|
|
d134aa0c20 | ||
|
|
b9f40f326f |
13
.github/issue_template.md
vendored
Normal file
13
.github/issue_template.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
### environment
|
||||
|
||||
* canal version
|
||||
* mysql version
|
||||
|
||||
### Issue Description
|
||||
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
### Expected behaviour
|
||||
|
||||
### Actual behaviour
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ jtester.properties
|
||||
.DS_Store
|
||||
*.tar.gz
|
||||
*.rpm
|
||||
client-adapter/example/
|
||||
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Executable file
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Executable file
Binary file not shown.
1
.mvn/wrapper/maven-wrapper.properties
vendored
Executable file
1
.mvn/wrapper/maven-wrapper.properties
vendored
Executable file
@@ -0,0 +1 @@
|
||||
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
|
||||
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
language: java
|
||||
sudo: false # faster builds
|
||||
|
||||
jdk:
|
||||
- openjdk8
|
||||
- oraclejdk8
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
|
||||
install: true
|
||||
|
||||
script:
|
||||
- travis_wait 30 ./mvnw clean install -DskipTests=true -Dcheckstyle.skip=true -Drat.skip=true -Dmaven.javadoc.skip=true
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
after_failure:
|
||||
- if [ -f canal.log ]; then echo "------TAIL of canal.log------"; tail -n 1000 canal.log; echo "------END of canal.log------"; fi
|
||||
14
README.md
14
README.md
@@ -1,6 +1,12 @@
|
||||
<div class="blog_content">
|
||||
<div class="iteye-blog-content-contain">
|
||||
|
||||
|
||||
[](https://travis-ci.com/alibaba/canal)
|
||||

|
||||

|
||||
[](http://isitmaintained.com/project/alibaba/canal "Average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/alibaba/canal "Percentage of issues still open")
|
||||
|
||||
<h1>背景</h1>
|
||||
<p style="font-size: 14px;"> 早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务,从此开启了一段新纪元。</p>
|
||||
<p style="font-size: 14px;"> ps. 目前内部版本已经支持mysql和oracle部分版本的日志解析,当前的canal开源版本支持5.7及以下的版本(阿里内部mysql 5.7.13, 5.6.10, mysql 5.5.18和5.1.40/48)</p>
|
||||
@@ -75,14 +81,14 @@ See the wiki page for : <a href="https://github.com/alibaba/canal/wiki" >wiki文
|
||||
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/TableMetaTSDB">TableMetaTSDB</a></li>
|
||||
<li><a href="http://alibaba.github.com/canal/release.html">ReleaseNotes</a></li>
|
||||
<li><a href="https://github.com/alibaba/canal/releases">Download</a></li>
|
||||
<li><a class="internal present" href="/alibaba/canal/wiki/FAQ">FAQ</a></li>
|
||||
<li><a class="internal present" href="https://github.com/alibaba/canal/wiki/FAQ">FAQ</a></li>
|
||||
</ul>
|
||||
|
||||
<h1>多语言业务</h1>
|
||||
|
||||
1. canal整体交互协议设计上使用了protobuf3.0,理论上可以支持绝大部分的多语言场景,欢迎大家提交多客户端的PR
|
||||
* canal java客户端: <a href="https://github.com/alibaba/canal/wiki/ClientExample"> https://github.com/alibaba/canal/wiki/ClientExample </a>
|
||||
* canal c#客户端开源项目地址:<a href="https://github.com/CanalClient/CanalSharp"> https://github.com/CanalSharp/CanalSharp </a>
|
||||
* canal c#客户端开源项目地址:<a href="https://github.com/dotnetcore/CanalSharp"> https://github.com/dotnetcore/CanalSharp </a>
|
||||
* canal go客户端开源项目地址:<a href="https://github.com/CanalClient/canal-go"> https://github.com/CanalClient/canal-go </a>
|
||||
2. canal作为MySQL binlog的增量获取工具,可以将数据投递到MQ系统中,比如Kafka/RocketMQ,可以借助于MQ的多语言能力
|
||||
* 参考文档: [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
|
||||
@@ -117,7 +123,7 @@ See the wiki page for : <a href="https://github.com/alibaba/canal/wiki" >wiki文
|
||||
<h3>最新更新</h3>
|
||||
<ol>
|
||||
<li>canal发布重大版本更新1.1.0,具体releaseNode参考:<a href="https://github.com/alibaba/canal/releases/tag/canal-1.1.0">https://github.com/alibaba/canal/releases/tag/canal-1.1.0</a></li>
|
||||
<li>canal c#客户端开源项目地址:<a href="https://github.com/CanalClient/CanalSharp"> https://github.com/CanalClient/CanalSharp </a>,推荐! </li>
|
||||
<li>canal c#客户端开源项目地址:<a href="https://github.com/dotnetcore/CanalSharp"> https://github.com/dotnetcore/CanalSharp </a>,推荐! </li>
|
||||
<li>canal QQ讨论群已经建立,群号:161559791 ,欢迎加入进行技术讨论。</li>
|
||||
<li>canal消费端项目开源: Otter(分布式数据库同步系统),地址:<a href="https://github.com/alibaba/otter">https://github.com/alibaba/otter</a></li>
|
||||
|
||||
|
||||
@@ -19,14 +19,20 @@ client-adapter分为适配器和启动器两部分, 适配器为多个fat jar,
|
||||
|
||||
启动器为 SpringBoot 项目, 支持canal-client启动的同时提供相关REST管理接口, 运行目录结构为:
|
||||
```
|
||||
canal-adapter-launcher.jar
|
||||
- bin
|
||||
restart.sh
|
||||
startup.bat
|
||||
startup.sh
|
||||
stop.sh
|
||||
- lib
|
||||
client-adapter.logger-1.1.1-jar-with-dependencies.jar
|
||||
client-adapter.hbase-1.1.1-jar-with-dependencies.jar
|
||||
- config
|
||||
...
|
||||
- conf
|
||||
application.yml
|
||||
- hbase
|
||||
mytest_person2.yml
|
||||
- logs
|
||||
```
|
||||
以上目录结构最终会打包成 canal-adapter-launcher.tar.gz 压缩包
|
||||
|
||||
@@ -49,7 +55,7 @@ canal.conf:
|
||||
hbase.zookeeper.quorum: slave1
|
||||
hbase.zookeeper.property.clientPort: 2181
|
||||
zookeeper.znode.parent: /hbase
|
||||
mqTopics: # MQ topic租, 如果是kafka或者rockeMQ模式可配置此项, 与canalInstances不能并存
|
||||
mqTopics: # MQ topic组, 如果是kafka或者rockeMQ模式可配置此项, 与canalInstances不能并存
|
||||
- mqMode: kafka # MQ的模式: kafak/rocketMQ
|
||||
topic: example # MQ topic
|
||||
groups: # group组
|
||||
@@ -96,7 +102,7 @@ curl http://127.0.0.1:8081/count/hbase/mytest_person2.yml
|
||||
```
|
||||
### 2.3 启动canal-adapter示例
|
||||
#### 2.3.1 启动canal server (单机模式), 参考: [Canal QuickStart](https://github.com/alibaba/canal/wiki/QuickStart)
|
||||
#### 2.3.2 修改config/application.yml为:
|
||||
#### 2.3.2 修改conf/application.yml为:
|
||||
```
|
||||
server:
|
||||
port: 8081
|
||||
@@ -118,9 +124,9 @@ canal.conf:
|
||||
- outAdapters:
|
||||
- name: logger
|
||||
```
|
||||
启动 canal-adapter-launcher.jar
|
||||
启动
|
||||
```
|
||||
java -jar canal-adapter-launcher.jar
|
||||
bin/startup.sh
|
||||
```
|
||||
|
||||
## 三、HBase适配器
|
||||
@@ -140,6 +146,11 @@ spring:
|
||||
canal.conf:
|
||||
canalServerHost: 127.0.0.1:11111
|
||||
flatMessage: true
|
||||
srcDataSources:
|
||||
defaultDS:
|
||||
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true
|
||||
username: root
|
||||
password: 121212
|
||||
canalInstances:
|
||||
- instance: example
|
||||
adapterGroups:
|
||||
@@ -149,19 +160,10 @@ canal.conf:
|
||||
hbase.zookeeper.quorum: slave1
|
||||
hbase.zookeeper.property.clientPort: 2181
|
||||
zookeeper.znode.parent: /hbase
|
||||
|
||||
adapter.conf:
|
||||
datasourceConfigs:
|
||||
defaultDS:
|
||||
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true
|
||||
username: root
|
||||
password: 121212
|
||||
adapterConfigs:
|
||||
- hbase/mytest_person.yml
|
||||
```
|
||||
其中指定了一个HBase表映射文件: mytest_person.yml
|
||||
adapter将会自动加载 conf/hbase 下的所有.yml结尾的配置文件
|
||||
### 3.2 适配器表映射文件
|
||||
修改 config/hbase/mytest_person.yml文件:
|
||||
修改 conf/hbase/mytest_person.yml文件:
|
||||
```
|
||||
dataSourceKey: defaultDS # 对应application.yml中的datasourceConfigs下的配置
|
||||
hbaseMapping: # mysql--HBase的单表映射配置
|
||||
@@ -245,7 +247,299 @@ create 'MYTEST.PERSON', {NAME=>'CF'}
|
||||
```
|
||||
#### 启动canal-adapter启动器
|
||||
```
|
||||
java -jar canal-adapter-launcher.jar
|
||||
bin/startup.sh
|
||||
```
|
||||
#### 验证
|
||||
修改mysql mytest.person表的数据, 将会自动同步到HBase的MYTEST.PERSON表下面, 并会打出DML的log
|
||||
|
||||
|
||||
## 四、关系型数据库适配器
|
||||
|
||||
RDB adapter 用于适配mysql到任意关系型数据库(需支持jdbc)的数据同步及导入
|
||||
|
||||
### 4.1 修改启动器配置: application.yml, 这里以oracle目标库为例
|
||||
```
|
||||
server:
|
||||
port: 8081
|
||||
logging:
|
||||
level:
|
||||
com.alibaba.otter.canal.client.adapter.rdb: DEBUG
|
||||
......
|
||||
|
||||
canal.conf:
|
||||
canalServerHost: 127.0.0.1:11111
|
||||
srcDataSources:
|
||||
defaultDS:
|
||||
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true
|
||||
username: root
|
||||
password: 121212
|
||||
canalInstances:
|
||||
- instance: example
|
||||
groups:
|
||||
- outAdapters:
|
||||
- name: rdb # 指定为rdb类型同步
|
||||
key: oracle1 # 指定adapter的唯一key, 与表映射配置中outerAdapterKey对应
|
||||
properties:
|
||||
jdbc.driverClassName: oracle.jdbc.OracleDriver # jdbc驱动名, jdbc的jar包需要自行放致lib目录下
|
||||
jdbc.url: jdbc:oracle:thin:@localhost:49161:XE # jdbc url
|
||||
jdbc.username: mytest # jdbc username
|
||||
jdbc.password: m121212 # jdbc password
|
||||
threads: 5 # 并行执行的线程数, 默认为1
|
||||
commitSize: 3000 # 批次提交的最大行数
|
||||
```
|
||||
其中 outAdapter 的配置: name统一为rdb, key为对应的数据源的唯一标识需和下面的表映射文件中的outerAdapterKey对应, properties为目标库jdb的相关参数
|
||||
adapter将会自动加载 conf/rdb 下的所有.yml结尾的表映射配置文件
|
||||
|
||||
### 4.2 适配器表映射文件
|
||||
修改 conf/rdb/mytest_user.yml文件:
|
||||
```
|
||||
dataSourceKey: defaultDS # 源数据源的key, 对应上面配置的srcDataSources中的值
|
||||
destination: example # cannal的instance或者MQ的topic
|
||||
outerAdapterKey: oracle1 # adapter key, 对应上面配置outAdapters中的key
|
||||
concurrent: true # 是否按主键hase并行同步, 并行同步的表必须保证主键不会更改及主键不能为其他同步表的外键!!
|
||||
dbMapping:
|
||||
database: mytest # 源数据源的database/shcema
|
||||
table: user # 源数据源表名
|
||||
targetTable: mytest.tb_user # 目标数据源的库名.表名
|
||||
targetPk: # 主键映射
|
||||
id: id # 如果是复合主键可以换行映射多个
|
||||
# mapAll: true # 是否整表映射, 要求源表和目标表字段名一模一样 (如果targetColumns也配置了映射,则以targetColumns配置为准)
|
||||
targetColumns: # 字段映射, 格式: 目标表字段: 源表字段, 如果字段名一样源表字段名可不填
|
||||
id:
|
||||
name:
|
||||
role_id:
|
||||
c_time:
|
||||
test1:
|
||||
```
|
||||
导入的类型以目标表的元类型为准, 将自动转换
|
||||
|
||||
### 4.3 启动RDB数据同步
|
||||
#### 将目标库的jdbc jar包放入lib文件夹, 这里放入ojdbc6.jar
|
||||
|
||||
#### 启动canal-adapter启动器
|
||||
```
|
||||
bin/startup.sh
|
||||
```
|
||||
#### 验证
|
||||
修改mysql mytest.user表的数据, 将会自动同步到Oracle的MYTEST.TB_USER表下面, 并会打出DML的log
|
||||
|
||||
|
||||
## 五、ElasticSearch适配器
|
||||
### 5.1 修改启动器配置: application.yml
|
||||
```
|
||||
server:
|
||||
port: 8081
|
||||
logging:
|
||||
level:
|
||||
com.alibaba.otter.canal.client.adapter.es: DEBUG
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
default-property-inclusion: non_null
|
||||
|
||||
canal.conf:
|
||||
canalServerHost: 127.0.0.1:11111
|
||||
flatMessage: true
|
||||
srcDataSources:
|
||||
defaultDS:
|
||||
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true
|
||||
username: root
|
||||
password: 121212
|
||||
canalInstances:
|
||||
- instance: example
|
||||
adapterGroups:
|
||||
- outAdapters:
|
||||
- name: es
|
||||
hosts: 127.0.0.1:9300 # es 集群地址, 逗号分隔
|
||||
properties:
|
||||
cluster.name: elasticsearch # es cluster name
|
||||
```
|
||||
adapter将会自动加载 conf/es 下的所有.yml结尾的配置文件
|
||||
### 5.2 适配器表映射文件
|
||||
修改 conf/es/mytest_user.yml文件:
|
||||
```
|
||||
dataSourceKey: defaultDS # 源数据源的key, 对应上面配置的srcDataSources中的值
|
||||
destination: example # cannal的instance或者MQ的topic
|
||||
esMapping:
|
||||
_index: mytest_user # es 的索引名称
|
||||
_type: _doc # es 的doc名称
|
||||
_id: _id # es 的_id, 如果不配置该项必须配置下面的pk项_id则会由es自动分配
|
||||
# pk: id # 如果不需要_id, 则需要指定一个属性为主键属性
|
||||
# sql映射
|
||||
sql: "select a.id as _id, a.name as _name, a.role_id as _role_id, b.role_name as _role_name,
|
||||
a.c_time as _c_time, c.labels as _labels from user a
|
||||
left join role b on b.id=a.role_id
|
||||
left join (select user_id, group_concat(label order by id desc separator ';') as labels from label
|
||||
group by user_id) c on c.user_id=a.id"
|
||||
# objFields:
|
||||
# _labels: array:; # 数组或者对象属性, array:; 代表以;字段里面是以;分隔的
|
||||
# _obj: obj:{"test":"123"}
|
||||
etlCondition: "where a.c_time>='{0}'" # etl 的条件参数
|
||||
commitBatch: 3000 # 提交批大小
|
||||
```
|
||||
sql映射说明:
|
||||
|
||||
sql支持多表关联自由组合, 但是有一定的限制:
|
||||
1. 主表不能为子查询语句
|
||||
2. 只能使用left outer join即最左表一定要是主表
|
||||
3. 关联从表如果是子查询不能有多张表
|
||||
4. 主sql中不能有where查询条件(从表子查询中可以有where条件但是不推荐, 可能会造成数据同步的不一致, 比如修改了where条件中的字段内容)
|
||||
5. 关联条件只允许主外键的'='操作不能出现其他常量判断比如: on a.role_id=b.id and b.statues=1
|
||||
6. 关联条件必须要有一个字段出现在主查询语句中比如: on a.role_id=b.id 其中的 a.role_id 或者 b.id 必须出现在主select语句中
|
||||
|
||||
|
||||
Elastic Search的mapping 属性与sql的查询值将一一对应(不支持 select *), 比如: select a.id as _id, a.name, a.email as _email from user, 其中name将映射到es mapping的name field, _email将
|
||||
映射到mapping的_email field, 这里以别名(如果有别名)作为最终的映射字段. 这里的_id可以填写到配置文件的 _id: _id映射.
|
||||
|
||||
#### 5.2.1.单表映射索引示例sql:
|
||||
```
|
||||
select a.id as _id, a.name, a.role_id, a.c_time from user a
|
||||
```
|
||||
该sql对应的es mapping示例:
|
||||
```
|
||||
{
|
||||
"mytest_user": {
|
||||
"mappings": {
|
||||
"_doc": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "text"
|
||||
},
|
||||
"role_id": {
|
||||
"type": "long"
|
||||
},
|
||||
"c_time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2.2.单表映射索引示例sql带函数或运算操作:
|
||||
```
|
||||
select a.id as _id, concat(a.name,'_test') as name, a.role_id+10000 as role_id, a.c_time from user a
|
||||
```
|
||||
函数字段后必须跟上别名, 该sql对应的es mapping示例:
|
||||
```
|
||||
{
|
||||
"mytest_user": {
|
||||
"mappings": {
|
||||
"_doc": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "text"
|
||||
},
|
||||
"role_id": {
|
||||
"type": "long"
|
||||
},
|
||||
"c_time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2.3.多表映射(一对一, 多对一)索引示例sql:
|
||||
```
|
||||
select a.id as _id, a.name, a.role_id, b.role_name, a.c_time from user a
|
||||
left join role b on b.id = a.role_id
|
||||
```
|
||||
注:这里join操作只能是left outer join, 第一张表必须为主表!!
|
||||
|
||||
该sql对应的es mapping示例:
|
||||
```
|
||||
{
|
||||
"mytest_user": {
|
||||
"mappings": {
|
||||
"_doc": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "text"
|
||||
},
|
||||
"role_id": {
|
||||
"type": "long"
|
||||
},
|
||||
"role_name": {
|
||||
"type": "text"
|
||||
},
|
||||
"c_time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2.4.多表映射(一对多)索引示例sql:
|
||||
```
|
||||
select a.id as _id, a.name, a.role_id, c.labels, a.c_time from user a
|
||||
left join (select user_id, group_concat(label order by id desc separator ';') as labels from label
|
||||
group by user_id) c on c.user_id=a.id
|
||||
```
|
||||
注:left join 后的子查询只允许一张表, 即子查询中不能再包含子查询或者关联!!
|
||||
|
||||
该sql对应的es mapping示例:
|
||||
```
|
||||
{
|
||||
"mytest_user": {
|
||||
"mappings": {
|
||||
"_doc": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "text"
|
||||
},
|
||||
"role_id": {
|
||||
"type": "long"
|
||||
},
|
||||
"c_time": {
|
||||
"type": "date"
|
||||
},
|
||||
"labels": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2.5.其它类型的sql示例:
|
||||
- geo type
|
||||
```
|
||||
select ... concat(IFNULL(a.latitude, 0), ',', IFNULL(a.longitude, 0)) AS location, ...
|
||||
```
|
||||
- 复合主键
|
||||
```
|
||||
select concat(a.id,'_',b.type) as _id, ... from user a left join role b on b.id=a.role_id
|
||||
```
|
||||
- 数组字段
|
||||
```
|
||||
select a.id as _id, a.name, a.role_id, c.labels, a.c_time from user a
|
||||
left join (select user_id, group_concat(label order by id desc separator ';') as labels from label
|
||||
group by user_id) c on c.user_id=a.id
|
||||
```
|
||||
配置中使用:
|
||||
```
|
||||
objFields:
|
||||
labels: array:;
|
||||
```
|
||||
|
||||
### 5.3 启动ES数据同步
|
||||
#### 启动canal-adapter启动器
|
||||
```
|
||||
bin/startup.sh
|
||||
```
|
||||
#### 验证
|
||||
1. 新增mysql mytest.user表的数据, 将会自动同步到es的mytest_user索引下面, 并会打出DML的log
|
||||
2. 修改mysql mytest.role表的role_name, 将会自动同步es的mytest_user索引中的role_name数据
|
||||
3. 新增或者修改mysql mytest.label表的label, 将会自动同步es的mytest_user索引中的labels数据
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.1</version>
|
||||
<version>1.1.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
@@ -14,7 +14,7 @@
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal.protocol</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<version>1.1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
@@ -27,9 +27,14 @@
|
||||
<version>1.1.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.40</version>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.19</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.alibaba.otter.canal.client.adapter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
@@ -21,15 +22,16 @@ public interface OuterAdapter {
|
||||
* 外部适配器初始化接口
|
||||
*
|
||||
* @param configuration 外部适配器配置信息
|
||||
* @param envProperties 环境变量的配置属性
|
||||
*/
|
||||
void init(OuterAdapterConfig configuration);
|
||||
void init(OuterAdapterConfig configuration, Properties envProperties);
|
||||
|
||||
/**
|
||||
* 往适配器中同步数据
|
||||
*
|
||||
* @param dml 数据包
|
||||
* @param dmls 数据包
|
||||
*/
|
||||
void sync(Dml dml);
|
||||
void sync(List<Dml> dmls);
|
||||
|
||||
/**
|
||||
* 外部适配器销毁接口
|
||||
@@ -38,7 +40,7 @@ public interface OuterAdapter {
|
||||
|
||||
/**
|
||||
* Etl操作
|
||||
*
|
||||
*
|
||||
* @param task 任务名, 对应配置名
|
||||
* @param params etl筛选条件
|
||||
*/
|
||||
@@ -48,7 +50,7 @@ public interface OuterAdapter {
|
||||
|
||||
/**
|
||||
* 计算总数
|
||||
*
|
||||
*
|
||||
* @param task 任务名, 对应配置名
|
||||
* @return 总数
|
||||
*/
|
||||
@@ -58,7 +60,7 @@ public interface OuterAdapter {
|
||||
|
||||
/**
|
||||
* 通过task获取对应的destination
|
||||
*
|
||||
*
|
||||
* @param task 任务名, 对应配置名
|
||||
* @return destination
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.bind.PropertiesConfigurationFactory;
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.*;
|
||||
|
||||
/**
|
||||
* 将yaml内容绑定到指定对象, 遵循spring yml的绑定规范
|
||||
*
|
||||
* @author reweerma 2019-2-1 上午9:14:02
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class YmlConfigBinder {
|
||||
|
||||
/**
|
||||
* 将当前内容绑定到指定对象
|
||||
*
|
||||
* @param content yml内容
|
||||
* @param clazz 指定对象类型
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T bindYmlToObj(String content, Class<T> clazz) {
|
||||
return bindYmlToObj(null, content, clazz, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前内容绑定到指定对象并指定内容编码格式
|
||||
*
|
||||
* @param content yml内容
|
||||
* @param clazz 指定对象类型
|
||||
* @param charset yml内容编码格式
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T bindYmlToObj(String content, Class<T> clazz, String charset) {
|
||||
return bindYmlToObj(null, content, clazz, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前内容指定前缀部分绑定到指定对象
|
||||
*
|
||||
* @param prefix 指定前缀
|
||||
* @param content yml内容
|
||||
* @param clazz 指定对象类型
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T bindYmlToObj(String prefix, String content, Class<T> clazz) {
|
||||
return bindYmlToObj(prefix, content, clazz, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前内容指定前缀部分绑定到指定对象并指定内容编码格式
|
||||
*
|
||||
* @param prefix 指定前缀
|
||||
* @param content yml内容
|
||||
* @param clazz 指定对象类型
|
||||
* @param charset yml内容编码格式
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T bindYmlToObj(String prefix, String content, Class<T> clazz, String charset) {
|
||||
return bindYmlToObj(prefix, content, clazz, charset, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前内容指定前缀部分绑定到指定对象并用环境变量中的属性替换占位符, 例: 当前内容有属性 zkServers: ${zookeeper.servers}
|
||||
* 在envProperties中有属性 zookeeper.servers:
|
||||
* 192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181 则当前内容 zkServers 会被替换为
|
||||
* zkServers: 192.168.0.1:2181,192.168.0.1:2181,192.168.0.1:2181 注: 假设绑定的类中
|
||||
* zkServers 属性是 List<String> 对象, 则会自动映射成List
|
||||
*
|
||||
* @param prefix 指定前缀
|
||||
* @param content yml内容
|
||||
* @param clazz 指定对象类型
|
||||
* @param charset yml内容编码格式
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T bindYmlToObj(String prefix, String content, Class<T> clazz, String charset,
|
||||
Properties baseProperties) {
|
||||
try {
|
||||
byte[] contentBytes;
|
||||
if (charset == null) {
|
||||
contentBytes = content.getBytes("UTF-8");
|
||||
} else {
|
||||
contentBytes = content.getBytes(charset);
|
||||
}
|
||||
YamlPropertySourceLoader propertySourceLoader = new YamlPropertySourceLoader();
|
||||
Resource configResource = new ByteArrayResource(contentBytes);
|
||||
PropertySource<?> propertySource = propertySourceLoader.load("manualBindConfig", configResource, null);
|
||||
|
||||
if (propertySource == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Properties properties = new Properties();
|
||||
Map<String, Object> propertiesRes = new LinkedHashMap<>();
|
||||
if (!StringUtils.isEmpty(prefix) && !prefix.endsWith(".")) {
|
||||
prefix = prefix + ".";
|
||||
}
|
||||
|
||||
properties.putAll((Map<?, ?>) propertySource.getSource());
|
||||
|
||||
if (baseProperties != null) {
|
||||
baseProperties.putAll(properties);
|
||||
properties = baseProperties;
|
||||
}
|
||||
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) propertySource.getSource()).entrySet()) {
|
||||
String key = (String) entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (prefix != null) {
|
||||
if (key != null && key.startsWith(prefix)) {
|
||||
key = key.substring(prefix.length());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof String && ((String) value).contains("${") && ((String) value).contains("}")) {
|
||||
PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}");
|
||||
value = propertyPlaceholderHelper.replacePlaceholders((String) value, properties);
|
||||
}
|
||||
|
||||
propertiesRes.put(key, value);
|
||||
}
|
||||
|
||||
if (propertiesRes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
propertySource = new MapPropertySource(propertySource.getName(), propertiesRes);
|
||||
|
||||
T target = clazz.newInstance();
|
||||
|
||||
MutablePropertySources propertySources = new MutablePropertySources();
|
||||
propertySources.addFirst(propertySource);
|
||||
|
||||
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(target);
|
||||
factory.setPropertySources(propertySources);
|
||||
factory.setIgnoreInvalidFields(true);
|
||||
factory.setIgnoreUnknownFields(true);
|
||||
|
||||
factory.bindPropertiesToTarget();
|
||||
|
||||
return target;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* {@link PropertyNamePatternsMatcher} that matches when a property name exactly
|
||||
* matches one of the given names, or starts with one of the given names
|
||||
* followed by a delimiter. This implementation is optimized for frequent calls.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.2.0
|
||||
*/
|
||||
class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
|
||||
|
||||
private final char[] delimiters;
|
||||
|
||||
private final boolean ignoreCase;
|
||||
|
||||
private final String[] names;
|
||||
|
||||
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, String... names){
|
||||
this(delimiters, false, names);
|
||||
}
|
||||
|
||||
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, String... names){
|
||||
this(delimiters, ignoreCase, new HashSet<String>(Arrays.asList(names)));
|
||||
}
|
||||
|
||||
DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase, Set<String> names){
|
||||
this.delimiters = delimiters;
|
||||
this.ignoreCase = ignoreCase;
|
||||
this.names = names.toArray(new String[names.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String propertyName) {
|
||||
char[] propertyNameChars = propertyName.toCharArray();
|
||||
boolean[] match = new boolean[this.names.length];
|
||||
boolean noneMatched = true;
|
||||
for (int i = 0; i < this.names.length; i++) {
|
||||
if (this.names[i].length() <= propertyNameChars.length) {
|
||||
match[i] = true;
|
||||
noneMatched = false;
|
||||
}
|
||||
}
|
||||
if (noneMatched) {
|
||||
return false;
|
||||
}
|
||||
for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) {
|
||||
for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) {
|
||||
if (match[nameIndex]) {
|
||||
match[nameIndex] = false;
|
||||
if (charIndex < this.names[nameIndex].length()) {
|
||||
if (isCharMatch(this.names[nameIndex].charAt(charIndex), propertyNameChars[charIndex])) {
|
||||
match[nameIndex] = true;
|
||||
noneMatched = false;
|
||||
}
|
||||
} else {
|
||||
char charAfter = propertyNameChars[this.names[nameIndex].length()];
|
||||
if (isDelimiter(charAfter)) {
|
||||
match[nameIndex] = true;
|
||||
noneMatched = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (noneMatched) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < match.length; i++) {
|
||||
if (match[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCharMatch(char c1, char c2) {
|
||||
if (this.ignoreCase) {
|
||||
return Character.toLowerCase(c1) == Character.toLowerCase(c2);
|
||||
}
|
||||
return c1 == c2;
|
||||
}
|
||||
|
||||
private boolean isDelimiter(char c) {
|
||||
for (char delimiter : this.delimiters) {
|
||||
if (c == delimiter) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* {@link PropertyNamePatternsMatcher} that matches when a property name exactly
|
||||
* matches one of the given names, or starts with one of the given names
|
||||
* followed by a delimiter. This implementation is optimized for frequent calls.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.2.0
|
||||
*/
|
||||
class InetAddressEditor extends PropertyEditorSupport {
|
||||
|
||||
@Override
|
||||
public String getAsText() {
|
||||
return ((InetAddress) getValue()).getHostAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
try {
|
||||
setValue(InetAddress.getByName(text));
|
||||
} catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException("Cannot locate host", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import org.springframework.beans.PropertyValue;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
|
||||
|
||||
/**
|
||||
* A {@link PropertyValue} that can provide information about its origin.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class OriginCapablePropertyValue extends PropertyValue {
|
||||
|
||||
private static final String ATTRIBUTE_PROPERTY_ORIGIN = "propertyOrigin";
|
||||
|
||||
private final PropertyOrigin origin;
|
||||
|
||||
OriginCapablePropertyValue(PropertyValue propertyValue){
|
||||
this(propertyValue.getName(),
|
||||
propertyValue.getValue(),
|
||||
(PropertyOrigin) propertyValue.getAttribute(ATTRIBUTE_PROPERTY_ORIGIN));
|
||||
}
|
||||
|
||||
OriginCapablePropertyValue(String name, Object value, String originName, PropertySource<?> originSource){
|
||||
this(name, value, new PropertyOrigin(originSource, originName));
|
||||
}
|
||||
|
||||
OriginCapablePropertyValue(String name, Object value, PropertyOrigin origin){
|
||||
super(name, value);
|
||||
this.origin = origin;
|
||||
setAttribute(ATTRIBUTE_PROPERTY_ORIGIN, origin);
|
||||
}
|
||||
|
||||
public PropertyOrigin getOrigin() {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String name = (this.origin != null ? this.origin.getName() : this.getName());
|
||||
String source = (this.origin.getSource() != null ? this.origin.getSource().getName() : "unknown");
|
||||
return "'" + name + "' from '" + source + "'";
|
||||
}
|
||||
|
||||
public static PropertyOrigin getOrigin(PropertyValue propertyValue) {
|
||||
if (propertyValue instanceof OriginCapablePropertyValue) {
|
||||
return ((OriginCapablePropertyValue) propertyValue).getOrigin();
|
||||
}
|
||||
return new OriginCapablePropertyValue(propertyValue).getOrigin();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
/**
|
||||
* {@link PropertyNamePatternsMatcher} that delegates to
|
||||
* {@link PatternMatchUtils#simpleMatch(String[], String)}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.2.0
|
||||
*/
|
||||
class PatternPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
|
||||
|
||||
private final String[] patterns;
|
||||
|
||||
PatternPropertyNamePatternsMatcher(Collection<String> patterns){
|
||||
this.patterns = (patterns != null ? patterns.toArray(new String[patterns.size()]) : new String[] {});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String propertyName) {
|
||||
return PatternMatchUtils.simpleMatch(this.patterns, propertyName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.support.ResourceEditorRegistrar;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.MessageSourceAware;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.*;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;
|
||||
|
||||
/**
|
||||
* Validate some {@link Properties} (or optionally
|
||||
* {@link org.springframework.core.env.PropertySources}) by binding them to an
|
||||
* object of a specified type and then optionally running a {@link Validator}
|
||||
* over it.
|
||||
*
|
||||
* @param <T> the target type
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class PropertiesConfigurationFactory<T> implements FactoryBean<T>, ApplicationContextAware, MessageSourceAware, InitializingBean {
|
||||
|
||||
private static final char[] EXACT_DELIMITERS = { '_', '.', '[' };
|
||||
|
||||
private static final char[] TARGET_NAME_DELIMITERS = { '_', '.' };
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PropertiesConfigurationFactory.class);
|
||||
|
||||
private boolean ignoreUnknownFields = true;
|
||||
|
||||
private boolean ignoreInvalidFields;
|
||||
|
||||
private boolean exceptionIfInvalid = true;
|
||||
|
||||
private PropertySources propertySources;
|
||||
|
||||
private final T target;
|
||||
|
||||
private Validator validator;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private MessageSource messageSource;
|
||||
|
||||
private boolean hasBeenBound = false;
|
||||
|
||||
private boolean ignoreNestedProperties = false;
|
||||
|
||||
private String targetName;
|
||||
|
||||
private ConversionService conversionService;
|
||||
|
||||
private boolean resolvePlaceholders = true;
|
||||
|
||||
/**
|
||||
* Create a new {@link PropertiesConfigurationFactory} instance.
|
||||
*
|
||||
* @param target the target object to bind too
|
||||
* @see #PropertiesConfigurationFactory(Class)
|
||||
*/
|
||||
public PropertiesConfigurationFactory(T target){
|
||||
Assert.notNull(target, "target must not be null");
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link PropertiesConfigurationFactory} instance.
|
||||
*
|
||||
* @param type the target type
|
||||
* @see #PropertiesConfigurationFactory(Class)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertiesConfigurationFactory(Class<?> type){
|
||||
Assert.notNull(type, "type must not be null");
|
||||
this.target = (T) BeanUtils.instantiate(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to disable binding of nested properties (i.e. those with period
|
||||
* separators in their paths). Can be useful to disable this if the name prefix
|
||||
* is empty and you don't want to ignore unknown fields.
|
||||
*
|
||||
* @param ignoreNestedProperties the flag to set (default false)
|
||||
*/
|
||||
public void setIgnoreNestedProperties(boolean ignoreNestedProperties) {
|
||||
this.ignoreNestedProperties = ignoreNestedProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to ignore unknown fields, that is, whether to ignore bind
|
||||
* parameters that do not have corresponding fields in the target object.
|
||||
* <p>
|
||||
* Default is "true". Turn this off to enforce that all bind parameters must
|
||||
* have a matching field in the target object.
|
||||
*
|
||||
* @param ignoreUnknownFields if unknown fields should be ignored
|
||||
*/
|
||||
public void setIgnoreUnknownFields(boolean ignoreUnknownFields) {
|
||||
this.ignoreUnknownFields = ignoreUnknownFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to ignore invalid fields, that is, whether to ignore bind
|
||||
* parameters that have corresponding fields in the target object which are not
|
||||
* accessible (for example because of null values in the nested path).
|
||||
* <p>
|
||||
* Default is "false". Turn this on to ignore bind parameters for nested objects
|
||||
* in non-existing parts of the target object graph.
|
||||
*
|
||||
* @param ignoreInvalidFields if invalid fields should be ignored
|
||||
*/
|
||||
public void setIgnoreInvalidFields(boolean ignoreInvalidFields) {
|
||||
this.ignoreInvalidFields = ignoreInvalidFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target name.
|
||||
*
|
||||
* @param targetName the target name
|
||||
*/
|
||||
public void setTargetName(String targetName) {
|
||||
this.targetName = targetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message source.
|
||||
*
|
||||
* @param messageSource the message source
|
||||
*/
|
||||
@Override
|
||||
public void setMessageSource(MessageSource messageSource) {
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the property sources.
|
||||
*
|
||||
* @param propertySources the property sources
|
||||
*/
|
||||
public void setPropertySources(PropertySources propertySources) {
|
||||
this.propertySources = propertySources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the conversion service.
|
||||
*
|
||||
* @param conversionService the conversion service
|
||||
*/
|
||||
public void setConversionService(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the validator.
|
||||
*
|
||||
* @param validator the validator
|
||||
*/
|
||||
public void setValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a flag to indicate that an exception should be raised if a Validator is
|
||||
* available and validation fails.
|
||||
*
|
||||
* @param exceptionIfInvalid the flag to set
|
||||
* @deprecated as of 1.5, do not specify a {@link Validator} if validation
|
||||
* should not occur
|
||||
*/
|
||||
@Deprecated
|
||||
public void setExceptionIfInvalid(boolean exceptionIfInvalid) {
|
||||
this.exceptionIfInvalid = exceptionIfInvalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to indicate that placeholders should be replaced during binding. Default
|
||||
* is true.
|
||||
*
|
||||
* @param resolvePlaceholders flag value
|
||||
*/
|
||||
public void setResolvePlaceholders(boolean resolvePlaceholders) {
|
||||
this.resolvePlaceholders = resolvePlaceholders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
bindPropertiesToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
if (this.target == null) {
|
||||
return Object.class;
|
||||
}
|
||||
return this.target.getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getObject() throws Exception {
|
||||
if (!this.hasBeenBound) {
|
||||
bindPropertiesToTarget();
|
||||
}
|
||||
return this.target;
|
||||
}
|
||||
|
||||
public void bindPropertiesToTarget() throws BindException {
|
||||
Assert.state(this.propertySources != null, "PropertySources should not be null");
|
||||
try {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Property Sources: " + this.propertySources);
|
||||
|
||||
}
|
||||
this.hasBeenBound = true;
|
||||
doBindPropertiesToTarget();
|
||||
} catch (BindException ex) {
|
||||
if (this.exceptionIfInvalid) {
|
||||
throw ex;
|
||||
}
|
||||
logger.error("Failed to load Properties validation bean. " + "Your Properties may be invalid.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void doBindPropertiesToTarget() throws BindException {
|
||||
RelaxedDataBinder dataBinder = (this.targetName != null ? new RelaxedDataBinder(this.target,
|
||||
this.targetName) : new RelaxedDataBinder(this.target));
|
||||
if (this.validator != null && this.validator.supports(dataBinder.getTarget().getClass())) {
|
||||
dataBinder.setValidator(this.validator);
|
||||
}
|
||||
if (this.conversionService != null) {
|
||||
dataBinder.setConversionService(this.conversionService);
|
||||
}
|
||||
dataBinder.setAutoGrowCollectionLimit(Integer.MAX_VALUE);
|
||||
dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties);
|
||||
dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields);
|
||||
dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields);
|
||||
customizeBinder(dataBinder);
|
||||
if (this.applicationContext != null) {
|
||||
ResourceEditorRegistrar resourceEditorRegistrar = new ResourceEditorRegistrar(this.applicationContext,
|
||||
this.applicationContext.getEnvironment());
|
||||
resourceEditorRegistrar.registerCustomEditors(dataBinder);
|
||||
}
|
||||
Iterable<String> relaxedTargetNames = getRelaxedTargetNames();
|
||||
Set<String> names = getNames(relaxedTargetNames);
|
||||
PropertyValues propertyValues = getPropertySourcesPropertyValues(names, relaxedTargetNames);
|
||||
dataBinder.bind(propertyValues);
|
||||
if (this.validator != null) {
|
||||
dataBinder.validate();
|
||||
}
|
||||
checkForBindingErrors(dataBinder);
|
||||
}
|
||||
|
||||
private Iterable<String> getRelaxedTargetNames() {
|
||||
return (this.target != null
|
||||
&& StringUtils.hasLength(this.targetName) ? new RelaxedNames(this.targetName) : null);
|
||||
}
|
||||
|
||||
private Set<String> getNames(Iterable<String> prefixes) {
|
||||
Set<String> names = new LinkedHashSet<String>();
|
||||
if (this.target != null) {
|
||||
PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(this.target.getClass());
|
||||
for (PropertyDescriptor descriptor : descriptors) {
|
||||
String name = descriptor.getName();
|
||||
if (!name.equals("class")) {
|
||||
RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name);
|
||||
if (prefixes == null) {
|
||||
for (String relaxedName : relaxedNames) {
|
||||
names.add(relaxedName);
|
||||
}
|
||||
} else {
|
||||
for (String prefix : prefixes) {
|
||||
for (String relaxedName : relaxedNames) {
|
||||
names.add(prefix + "." + relaxedName);
|
||||
names.add(prefix + "_" + relaxedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private PropertyValues getPropertySourcesPropertyValues(Set<String> names, Iterable<String> relaxedTargetNames) {
|
||||
PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names, relaxedTargetNames);
|
||||
return new PropertySourcesPropertyValues(this.propertySources, names, includes, this.resolvePlaceholders);
|
||||
}
|
||||
|
||||
private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set<String> names,
|
||||
Iterable<String> relaxedTargetNames) {
|
||||
if (this.ignoreUnknownFields && !isMapTarget()) {
|
||||
// Since unknown fields are ignored we can filter them out early to save
|
||||
// unnecessary calls to the PropertySource.
|
||||
return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMITERS, true, names);
|
||||
}
|
||||
if (relaxedTargetNames != null) {
|
||||
// We can filter properties to those starting with the target name, but
|
||||
// we can't do a complete filter since we need to trigger the
|
||||
// unknown fields check
|
||||
Set<String> relaxedNames = new HashSet<String>();
|
||||
for (String relaxedTargetName : relaxedTargetNames) {
|
||||
relaxedNames.add(relaxedTargetName);
|
||||
}
|
||||
return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMITERS, true, relaxedNames);
|
||||
}
|
||||
// Not ideal, we basically can't filter anything
|
||||
return PropertyNamePatternsMatcher.ALL;
|
||||
}
|
||||
|
||||
private boolean isMapTarget() {
|
||||
return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
|
||||
}
|
||||
|
||||
private void checkForBindingErrors(RelaxedDataBinder dataBinder) throws BindException {
|
||||
BindingResult errors = dataBinder.getBindingResult();
|
||||
if (errors.hasErrors()) {
|
||||
logger.error("Properties configuration failed validation");
|
||||
for (ObjectError error : errors.getAllErrors()) {
|
||||
logger.error(this.messageSource != null ? this.messageSource.getMessage(error, Locale.getDefault())
|
||||
+ " (" + error + ")" : error);
|
||||
}
|
||||
if (this.exceptionIfInvalid) {
|
||||
throw new BindException(errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the data binder.
|
||||
*
|
||||
* @param dataBinder the data binder that will be used to bind and validate
|
||||
*/
|
||||
protected void customizeBinder(DataBinder dataBinder) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
/**
|
||||
* Strategy interface used to check if a property name matches specific
|
||||
* criteria.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.2.0
|
||||
*/
|
||||
interface PropertyNamePatternsMatcher {
|
||||
|
||||
PropertyNamePatternsMatcher ALL = new PropertyNamePatternsMatcher() {
|
||||
|
||||
@Override
|
||||
public boolean matches(String propertyName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
PropertyNamePatternsMatcher NONE = new PropertyNamePatternsMatcher() {
|
||||
|
||||
@Override
|
||||
public boolean matches(String propertyName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Return {@code true} of the property name matches.
|
||||
*
|
||||
* @param propertyName the property name
|
||||
* @return {@code true} if the property name matches
|
||||
*/
|
||||
boolean matches(String propertyName);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
|
||||
|
||||
/**
|
||||
* The origin of a property, specifically its source and its name before any
|
||||
* prefix was removed.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class PropertyOrigin {
|
||||
|
||||
private final PropertySource<?> source;
|
||||
|
||||
private final String name;
|
||||
|
||||
PropertyOrigin(PropertySource<?> source, String name){
|
||||
this.name = name;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public PropertySource<?> getSource() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import org.springframework.core.convert.ConversionException;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.AbstractPropertyResolver;
|
||||
import org.springframework.core.env.PropertyResolver;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;
|
||||
|
||||
/**
|
||||
* {@link PropertyResolver} implementation that resolves property values against
|
||||
* an underlying set of {@link PropertySources}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @see PropertySource
|
||||
* @see PropertySources
|
||||
* @see AbstractEnvironment
|
||||
* @since 3.1
|
||||
*/
|
||||
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
|
||||
|
||||
private final PropertySources propertySources;
|
||||
|
||||
/**
|
||||
* Create a new resolver against the given property sources.
|
||||
*
|
||||
* @param propertySources the set of {@link PropertySource} objects to use
|
||||
*/
|
||||
public PropertySourcesPropertyResolver(PropertySources propertySources){
|
||||
this.propertySources = propertySources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsProperty(String key) {
|
||||
if (this.propertySources != null) {
|
||||
for (PropertySource<?> propertySource : this.propertySources) {
|
||||
if (propertySource.containsProperty(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProperty(String key) {
|
||||
return getProperty(key, String.class, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getProperty(String key, Class<T> targetValueType) {
|
||||
return getProperty(key, targetValueType, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPropertyAsRawString(String key) {
|
||||
return getProperty(key, String.class, false);
|
||||
}
|
||||
|
||||
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
|
||||
if (this.propertySources != null) {
|
||||
for (PropertySource<?> propertySource : this.propertySources) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger
|
||||
.trace("Searching for key '" + key + "' in PropertySource '" + propertySource.getName() + "'");
|
||||
}
|
||||
Object value = propertySource.getProperty(key);
|
||||
if (value != null) {
|
||||
if (resolveNestedPlaceholders && value instanceof String) {
|
||||
value = resolveNestedPlaceholders((String) value);
|
||||
}
|
||||
logKeyFound(key, propertySource, value);
|
||||
return convertValueIfNecessary(value, targetValueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not find key '" + key + "' in any property source");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public <T> Class<T> getPropertyAsClass(String key, Class<T> targetValueType) {
|
||||
if (this.propertySources != null) {
|
||||
for (PropertySource<?> propertySource : this.propertySources) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(String.format("Searching for key '%s' in [%s]", key, propertySource.getName()));
|
||||
}
|
||||
Object value = propertySource.getProperty(key);
|
||||
if (value != null) {
|
||||
logKeyFound(key, propertySource, value);
|
||||
Class<?> clazz;
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
clazz = ClassUtils.forName((String) value, null);
|
||||
} catch (Exception ex) {
|
||||
throw new PropertySourcesPropertyResolver.ClassConversionException((String) value,
|
||||
targetValueType,
|
||||
ex);
|
||||
}
|
||||
} else if (value instanceof Class) {
|
||||
clazz = (Class<?>) value;
|
||||
} else {
|
||||
clazz = value.getClass();
|
||||
}
|
||||
if (!targetValueType.isAssignableFrom(clazz)) {
|
||||
throw new PropertySourcesPropertyResolver.ClassConversionException(clazz, targetValueType);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<T> targetClass = (Class<T>) clazz;
|
||||
return targetClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Could not find key '%s' in any property source", key));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given key as found in the given {@link PropertySource}, resulting in
|
||||
* the given value.
|
||||
* <p>
|
||||
* The default implementation writes a debug log message with key and source. As
|
||||
* of 4.3.3, this does not log the value anymore in order to avoid accidental
|
||||
* logging of sensitive settings. Subclasses may override this method to change
|
||||
* the log level and/or log message, including the property's value if desired.
|
||||
*
|
||||
* @param key the key found
|
||||
* @param propertySource the {@code PropertySource} that the key has been found
|
||||
* in
|
||||
* @param value the corresponding value
|
||||
* @since 4.3.1
|
||||
*/
|
||||
protected void logKeyFound(String key, PropertySource<?> propertySource, Object value) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Found key '" + key + "' in PropertySource '" + propertySource.getName()
|
||||
+ "' with value of type " + value.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Deprecated
|
||||
private static class ClassConversionException extends ConversionException {
|
||||
|
||||
public ClassConversionException(Class<?> actual, Class<?> expected){
|
||||
super(String
|
||||
.format("Actual type %s is not assignable to expected type %s", actual.getName(), expected.getName()));
|
||||
}
|
||||
|
||||
public ClassConversionException(String actual, Class<?> expected, Exception ex){
|
||||
super(
|
||||
String
|
||||
.format("Could not find/load class %s during attempt to convert to %s", actual, expected.getName()),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.DataBinder;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.CompositePropertySource;
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.EnumerablePropertySource;
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySource;
|
||||
import com.alibaba.otter.canal.client.adapter.config.common.PropertySources;
|
||||
|
||||
/**
|
||||
* A {@link PropertyValues} implementation backed by a {@link PropertySources},
|
||||
* bridging the two abstractions and allowing (for instance) a regular
|
||||
* {@link DataBinder} to be used with the latter.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class PropertySourcesPropertyValues implements PropertyValues {
|
||||
|
||||
private static final Pattern COLLECTION_PROPERTY = Pattern
|
||||
.compile("\\[(\\d+)\\](\\.\\S+)?");
|
||||
|
||||
private final PropertySources propertySources;
|
||||
|
||||
private final Collection<String> nonEnumerableFallbackNames;
|
||||
|
||||
private final PropertyNamePatternsMatcher includes;
|
||||
|
||||
private final Map<String, PropertyValue> propertyValues = new LinkedHashMap<String, PropertyValue>();
|
||||
|
||||
private final ConcurrentHashMap<String, PropertySource<?>> collectionOwners = new ConcurrentHashMap<String, PropertySource<?>>();
|
||||
|
||||
private final boolean resolvePlaceholders;
|
||||
|
||||
/**
|
||||
* Create a new PropertyValues from the given PropertySources.
|
||||
*
|
||||
* @param propertySources a PropertySources instance
|
||||
*/
|
||||
public PropertySourcesPropertyValues(PropertySources propertySources){
|
||||
this(propertySources, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PropertyValues from the given PropertySources that will
|
||||
* optionally resolve placeholders.
|
||||
*
|
||||
* @param propertySources a PropertySources instance
|
||||
* @param resolvePlaceholders {@code true} if placeholders should be resolved.
|
||||
* @since 1.5.2
|
||||
*/
|
||||
public PropertySourcesPropertyValues(PropertySources propertySources, boolean resolvePlaceholders){
|
||||
this(propertySources, (Collection<String>) null, PropertyNamePatternsMatcher.ALL, resolvePlaceholders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PropertyValues from the given PropertySources.
|
||||
*
|
||||
* @param propertySources a PropertySources instance
|
||||
* @param includePatterns property name patterns to include from system
|
||||
* properties and environment variables
|
||||
* @param nonEnumerableFallbackNames the property names to try in lieu of an
|
||||
* {@link EnumerablePropertySource}.
|
||||
*/
|
||||
public PropertySourcesPropertyValues(PropertySources propertySources, Collection<String> includePatterns,
|
||||
Collection<String> nonEnumerableFallbackNames){
|
||||
this(propertySources,
|
||||
nonEnumerableFallbackNames,
|
||||
new PatternPropertyNamePatternsMatcher(includePatterns),
|
||||
true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PropertyValues from the given PropertySources.
|
||||
*
|
||||
* @param propertySources a PropertySources instance
|
||||
* @param nonEnumerableFallbackNames the property names to try in lieu of an
|
||||
* {@link EnumerablePropertySource}.
|
||||
* @param includes the property name patterns to include
|
||||
* @param resolvePlaceholders flag to indicate the placeholders should be
|
||||
* resolved
|
||||
*/
|
||||
PropertySourcesPropertyValues(PropertySources propertySources, Collection<String> nonEnumerableFallbackNames,
|
||||
PropertyNamePatternsMatcher includes, boolean resolvePlaceholders){
|
||||
Assert.notNull(propertySources, "PropertySources must not be null");
|
||||
Assert.notNull(includes, "Includes must not be null");
|
||||
this.propertySources = propertySources;
|
||||
this.nonEnumerableFallbackNames = nonEnumerableFallbackNames;
|
||||
this.includes = includes;
|
||||
this.resolvePlaceholders = resolvePlaceholders;
|
||||
PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
|
||||
for (PropertySource<?> source : propertySources) {
|
||||
processPropertySource(source, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
private void processPropertySource(PropertySource<?> source, PropertySourcesPropertyResolver resolver) {
|
||||
if (source instanceof CompositePropertySource) {
|
||||
processCompositePropertySource((CompositePropertySource) source, resolver);
|
||||
} else if (source instanceof EnumerablePropertySource) {
|
||||
processEnumerablePropertySource((EnumerablePropertySource<?>) source, resolver, this.includes);
|
||||
} else {
|
||||
processNonEnumerablePropertySource(source, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
private void processCompositePropertySource(CompositePropertySource source,
|
||||
PropertySourcesPropertyResolver resolver) {
|
||||
for (PropertySource<?> nested : source.getPropertySources()) {
|
||||
processPropertySource(nested, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
private void processEnumerablePropertySource(EnumerablePropertySource<?> source,
|
||||
PropertySourcesPropertyResolver resolver,
|
||||
PropertyNamePatternsMatcher includes) {
|
||||
if (source.getPropertyNames().length > 0) {
|
||||
for (String propertyName : source.getPropertyNames()) {
|
||||
if (includes.matches(propertyName)) {
|
||||
Object value = getEnumerableProperty(source, resolver, propertyName);
|
||||
putIfAbsent(propertyName, value, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object getEnumerableProperty(EnumerablePropertySource<?> source, PropertySourcesPropertyResolver resolver,
|
||||
String propertyName) {
|
||||
try {
|
||||
if (this.resolvePlaceholders) {
|
||||
return resolver.getProperty(propertyName, Object.class);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
// Probably could not resolve placeholders, ignore it here
|
||||
}
|
||||
return source.getProperty(propertyName);
|
||||
}
|
||||
|
||||
private void processNonEnumerablePropertySource(PropertySource<?> source,
|
||||
PropertySourcesPropertyResolver resolver) {
|
||||
// We can only do exact matches for non-enumerable property names, but
|
||||
// that's better than nothing...
|
||||
if (this.nonEnumerableFallbackNames == null) {
|
||||
return;
|
||||
}
|
||||
for (String propertyName : this.nonEnumerableFallbackNames) {
|
||||
if (!source.containsProperty(propertyName)) {
|
||||
continue;
|
||||
}
|
||||
Object value = null;
|
||||
try {
|
||||
value = resolver.getProperty(propertyName, Object.class);
|
||||
} catch (RuntimeException ex) {
|
||||
// Probably could not convert to Object, weird, but ignorable
|
||||
}
|
||||
if (value == null) {
|
||||
value = source.getProperty(propertyName.toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
putIfAbsent(propertyName, value, source);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyValue[] getPropertyValues() {
|
||||
Collection<PropertyValue> values = this.propertyValues.values();
|
||||
return values.toArray(new PropertyValue[values.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyValue getPropertyValue(String propertyName) {
|
||||
PropertyValue propertyValue = this.propertyValues.get(propertyName);
|
||||
if (propertyValue != null) {
|
||||
return propertyValue;
|
||||
}
|
||||
for (PropertySource<?> source : this.propertySources) {
|
||||
Object value = source.getProperty(propertyName);
|
||||
propertyValue = putIfAbsent(propertyName, value, source);
|
||||
if (propertyValue != null) {
|
||||
return propertyValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private PropertyValue putIfAbsent(String propertyName, Object value, PropertySource<?> source) {
|
||||
if (value != null && !this.propertyValues.containsKey(propertyName)) {
|
||||
PropertySource<?> collectionOwner = this.collectionOwners
|
||||
.putIfAbsent(COLLECTION_PROPERTY.matcher(propertyName).replaceAll("[]"), source);
|
||||
if (collectionOwner == null || collectionOwner == source) {
|
||||
PropertyValue propertyValue = new OriginCapablePropertyValue(propertyName, value, propertyName, source);
|
||||
this.propertyValues.put(propertyName, propertyValue);
|
||||
return propertyValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyValues changesSince(PropertyValues old) {
|
||||
MutablePropertyValues changes = new MutablePropertyValues();
|
||||
// for each property value in the new set
|
||||
for (PropertyValue newValue : getPropertyValues()) {
|
||||
// if there wasn't an old one, add it
|
||||
PropertyValue oldValue = old.getPropertyValue(newValue.getName());
|
||||
if (oldValue == null || !oldValue.equals(newValue)) {
|
||||
changes.addPropertyValue(newValue);
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String propertyName) {
|
||||
return getPropertyValue(propertyName) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return this.propertyValues.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Internal {@link ConversionService} used by {@link RelaxedDataBinder} to
|
||||
* support additional relaxed conversion.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @since 1.1.0
|
||||
*/
|
||||
class RelaxedConversionService implements ConversionService {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private final GenericConversionService additionalConverters;
|
||||
|
||||
/**
|
||||
* Create a new {@link RelaxedConversionService} instance.
|
||||
*
|
||||
* @param conversionService and option root conversion service
|
||||
*/
|
||||
RelaxedConversionService(ConversionService conversionService){
|
||||
this.conversionService = conversionService;
|
||||
this.additionalConverters = new GenericConversionService();
|
||||
DefaultConversionService.addCollectionConverters(this.additionalConverters);
|
||||
this.additionalConverters
|
||||
.addConverterFactory(new RelaxedConversionService.StringToEnumIgnoringCaseConverterFactory());
|
||||
this.additionalConverters.addConverter(new StringToCharArrayConverter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
|
||||
return (this.conversionService != null && this.conversionService.canConvert(sourceType, targetType))
|
||||
|| this.additionalConverters.canConvert(sourceType, targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
return (this.conversionService != null && this.conversionService.canConvert(sourceType, targetType))
|
||||
|| this.additionalConverters.canConvert(sourceType, targetType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T convert(Object source, Class<T> targetType) {
|
||||
Assert.notNull(targetType, "The targetType to convert to cannot be null");
|
||||
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (this.conversionService != null) {
|
||||
try {
|
||||
return this.conversionService.convert(source, sourceType, targetType);
|
||||
} catch (ConversionFailedException ex) {
|
||||
// Ignore and try the additional converters
|
||||
}
|
||||
}
|
||||
return this.additionalConverters.convert(source, sourceType, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone of Spring's package private StringToEnumConverterFactory, but ignoring
|
||||
* the case of the source.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private static class StringToEnumIgnoringCaseConverterFactory implements ConverterFactory<String, Enum> {
|
||||
|
||||
@Override
|
||||
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
|
||||
Class<?> enumType = targetType;
|
||||
while (enumType != null && !enumType.isEnum()) {
|
||||
enumType = enumType.getSuperclass();
|
||||
}
|
||||
Assert.notNull(enumType, "The target type " + targetType.getName() + " does not refer to an enum");
|
||||
return new RelaxedConversionService.StringToEnumIgnoringCaseConverterFactory.StringToEnum(enumType);
|
||||
}
|
||||
|
||||
private static class StringToEnum<T extends Enum> implements Converter<String, T> {
|
||||
|
||||
private final Class<T> enumType;
|
||||
|
||||
StringToEnum(Class<T> enumType){
|
||||
this.enumType = enumType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T convert(String source) {
|
||||
if (source.isEmpty()) {
|
||||
// It's an empty enum identifier: reset the enum value to null.
|
||||
return null;
|
||||
}
|
||||
source = source.trim();
|
||||
for (T candidate : (Set<T>) EnumSet.allOf(this.enumType)) {
|
||||
RelaxedNames names = new RelaxedNames(
|
||||
candidate.name().replace('_', '-').toLowerCase(Locale.ENGLISH));
|
||||
for (String name : names) {
|
||||
if (name.equals(source)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (candidate.name().equalsIgnoreCase(source)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"No enum constant " + this.enumType.getCanonicalName() + "." + source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.net.InetAddress;
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.beans.*;
|
||||
import org.springframework.beans.propertyeditors.FileEditor;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.AbstractPropertyBindingResult;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.DataBinder;
|
||||
|
||||
/**
|
||||
* Binder implementation that allows caller to bind to maps and also allows
|
||||
* property names to match a bit loosely (if underscores or dashes are removed
|
||||
* and replaced with camel case for example).
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @see RelaxedNames
|
||||
*/
|
||||
public class RelaxedDataBinder extends DataBinder {
|
||||
|
||||
private static final Set<Class<?>> EXCLUDED_EDITORS;
|
||||
|
||||
static {
|
||||
Set<Class<?>> excluded = new HashSet<Class<?>>();
|
||||
excluded.add(FileEditor.class);
|
||||
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
|
||||
}
|
||||
|
||||
private static final Object BLANK = new Object();
|
||||
|
||||
private String namePrefix;
|
||||
|
||||
private boolean ignoreNestedProperties;
|
||||
|
||||
private MultiValueMap<String, String> nameAliases = new LinkedMultiValueMap<String, String>();
|
||||
|
||||
/**
|
||||
* Create a new {@link RelaxedDataBinder} instance.
|
||||
*
|
||||
* @param target the target into which properties are bound
|
||||
*/
|
||||
public RelaxedDataBinder(Object target){
|
||||
super(wrapTarget(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RelaxedDataBinder} instance.
|
||||
*
|
||||
* @param target the target into which properties are bound
|
||||
* @param namePrefix An optional prefix to be used when reading properties
|
||||
*/
|
||||
public RelaxedDataBinder(Object target, String namePrefix){
|
||||
super(wrapTarget(target), (StringUtils.hasLength(namePrefix) ? namePrefix : DEFAULT_OBJECT_NAME));
|
||||
this.namePrefix = cleanNamePrefix(namePrefix);
|
||||
}
|
||||
|
||||
private String cleanNamePrefix(String namePrefix) {
|
||||
if (!StringUtils.hasLength(namePrefix)) {
|
||||
return null;
|
||||
}
|
||||
return (namePrefix.endsWith(".") ? namePrefix : namePrefix + ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to disable binding of nested properties (i.e. those with period
|
||||
* separators in their paths). Can be useful to disable this if the name prefix
|
||||
* is empty and you don't want to ignore unknown fields.
|
||||
*
|
||||
* @param ignoreNestedProperties the flag to set (default false)
|
||||
*/
|
||||
public void setIgnoreNestedProperties(boolean ignoreNestedProperties) {
|
||||
this.ignoreNestedProperties = ignoreNestedProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name aliases.
|
||||
*
|
||||
* @param aliases a map of property name to aliases
|
||||
*/
|
||||
public void setNameAliases(Map<String, List<String>> aliases) {
|
||||
this.nameAliases = new LinkedMultiValueMap<String, String>(aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add aliases to the {@link DataBinder}.
|
||||
*
|
||||
* @param name the property name to alias
|
||||
* @param alias aliases for the property names
|
||||
* @return this instance
|
||||
*/
|
||||
public RelaxedDataBinder withAlias(String name, String... alias) {
|
||||
for (String value : alias) {
|
||||
this.nameAliases.add(name, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doBind(MutablePropertyValues propertyValues) {
|
||||
super.doBind(modifyProperties(propertyValues, getTarget()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the property values so that period separated property paths are valid
|
||||
* for map keys. Also creates new maps for properties of map type that are null
|
||||
* (assuming all maps are potentially nested). The standard bracket {@code[...]}
|
||||
* dereferencing is also accepted.
|
||||
*
|
||||
* @param propertyValues the property values
|
||||
* @param target the target object
|
||||
* @return modified property values
|
||||
*/
|
||||
private MutablePropertyValues modifyProperties(MutablePropertyValues propertyValues, Object target) {
|
||||
propertyValues = getPropertyValuesForNamePrefix(propertyValues);
|
||||
if (target instanceof RelaxedDataBinder.MapHolder) {
|
||||
propertyValues = addMapPrefix(propertyValues);
|
||||
}
|
||||
BeanWrapper wrapper = new BeanWrapperImpl(target);
|
||||
wrapper.setConversionService(new RelaxedConversionService(getConversionService()));
|
||||
wrapper.setAutoGrowNestedPaths(true);
|
||||
List<PropertyValue> sortedValues = new ArrayList<PropertyValue>();
|
||||
Set<String> modifiedNames = new HashSet<String>();
|
||||
List<String> sortedNames = getSortedPropertyNames(propertyValues);
|
||||
for (String name : sortedNames) {
|
||||
PropertyValue propertyValue = propertyValues.getPropertyValue(name);
|
||||
PropertyValue modifiedProperty = modifyProperty(wrapper, propertyValue);
|
||||
if (modifiedNames.add(modifiedProperty.getName())) {
|
||||
sortedValues.add(modifiedProperty);
|
||||
}
|
||||
}
|
||||
return new MutablePropertyValues(sortedValues);
|
||||
}
|
||||
|
||||
private List<String> getSortedPropertyNames(MutablePropertyValues propertyValues) {
|
||||
List<String> names = new LinkedList<String>();
|
||||
for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) {
|
||||
names.add(propertyValue.getName());
|
||||
}
|
||||
sortPropertyNames(names);
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by name so that parent properties get processed first (e.g. 'foo.bar'
|
||||
* before 'foo.bar.spam'). Don't use Collections.sort() because the order might
|
||||
* be significant for other property names (it shouldn't be but who knows what
|
||||
* people might be relying on, e.g. HSQL has a JDBCXADataSource where
|
||||
* "databaseName" is a synonym for "url").
|
||||
*
|
||||
* @param names the names to sort
|
||||
*/
|
||||
private void sortPropertyNames(List<String> names) {
|
||||
for (String name : new ArrayList<String>(names)) {
|
||||
int propertyIndex = names.indexOf(name);
|
||||
RelaxedDataBinder.BeanPath path = new RelaxedDataBinder.BeanPath(name);
|
||||
for (String prefix : path.prefixes()) {
|
||||
int prefixIndex = names.indexOf(prefix);
|
||||
if (prefixIndex >= propertyIndex) {
|
||||
// The child property has a parent in the list in the wrong order
|
||||
names.remove(name);
|
||||
names.add(prefixIndex, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MutablePropertyValues addMapPrefix(MutablePropertyValues propertyValues) {
|
||||
MutablePropertyValues rtn = new MutablePropertyValues();
|
||||
for (PropertyValue pv : propertyValues.getPropertyValues()) {
|
||||
rtn.add("map." + pv.getName(), pv.getValue());
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
private MutablePropertyValues getPropertyValuesForNamePrefix(MutablePropertyValues propertyValues) {
|
||||
if (!StringUtils.hasText(this.namePrefix) && !this.ignoreNestedProperties) {
|
||||
return propertyValues;
|
||||
}
|
||||
MutablePropertyValues rtn = new MutablePropertyValues();
|
||||
for (PropertyValue value : propertyValues.getPropertyValues()) {
|
||||
String name = value.getName();
|
||||
for (String prefix : new RelaxedNames(stripLastDot(this.namePrefix))) {
|
||||
for (String separator : new String[] { ".", "_" }) {
|
||||
String candidate = (StringUtils.hasLength(prefix) ? prefix + separator : prefix);
|
||||
if (name.startsWith(candidate)) {
|
||||
name = name.substring(candidate.length());
|
||||
if (!(this.ignoreNestedProperties && name.contains("."))) {
|
||||
PropertyOrigin propertyOrigin = OriginCapablePropertyValue.getOrigin(value);
|
||||
rtn.addPropertyValue(
|
||||
new OriginCapablePropertyValue(name, value.getValue(), propertyOrigin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
private String stripLastDot(String string) {
|
||||
if (StringUtils.hasLength(string) && string.endsWith(".")) {
|
||||
string = string.substring(0, string.length() - 1);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
private PropertyValue modifyProperty(BeanWrapper target, PropertyValue propertyValue) {
|
||||
String name = propertyValue.getName();
|
||||
String normalizedName = normalizePath(target, name);
|
||||
if (!normalizedName.equals(name)) {
|
||||
return new PropertyValue(normalizedName, propertyValue.getValue());
|
||||
}
|
||||
return propertyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a bean property path to a format understood by a BeanWrapper. This
|
||||
* is used so that
|
||||
* <ul>
|
||||
* <li>Fuzzy matching can be employed for bean property names</li>
|
||||
* <li>Period separators can be used instead of indexing ([...]) for map
|
||||
* keys</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param wrapper a bean wrapper for the object to bind
|
||||
* @param path the bean path to bind
|
||||
* @return a transformed path with correct bean wrapper syntax
|
||||
*/
|
||||
protected String normalizePath(BeanWrapper wrapper, String path) {
|
||||
return initializePath(wrapper, new RelaxedDataBinder.BeanPath(path), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
|
||||
return new RelaxedDataBinder.RelaxedBeanPropertyBindingResult(getTarget(),
|
||||
getObjectName(),
|
||||
isAutoGrowNestedPaths(),
|
||||
getAutoGrowCollectionLimit(),
|
||||
getConversionService());
|
||||
}
|
||||
|
||||
private String initializePath(BeanWrapper wrapper, RelaxedDataBinder.BeanPath path, int index) {
|
||||
String prefix = path.prefix(index);
|
||||
String key = path.name(index);
|
||||
if (path.isProperty(index)) {
|
||||
key = getActualPropertyName(wrapper, prefix, key);
|
||||
path.rename(index, key);
|
||||
}
|
||||
if (path.name(++index) == null) {
|
||||
return path.toString();
|
||||
}
|
||||
String name = path.prefix(index);
|
||||
TypeDescriptor descriptor = wrapper.getPropertyTypeDescriptor(name);
|
||||
if (descriptor == null || descriptor.isMap()) {
|
||||
if (isMapValueStringType(descriptor) || isBlanked(wrapper, name, path.name(index))) {
|
||||
path.collapseKeys(index);
|
||||
}
|
||||
path.mapIndex(index);
|
||||
extendMapIfNecessary(wrapper, path, index);
|
||||
} else if (descriptor.isCollection()) {
|
||||
extendCollectionIfNecessary(wrapper, path, index);
|
||||
} else if (descriptor.getType().equals(Object.class)) {
|
||||
if (isBlanked(wrapper, name, path.name(index))) {
|
||||
path.collapseKeys(index);
|
||||
}
|
||||
path.mapIndex(index);
|
||||
if (path.isLastNode(index)) {
|
||||
wrapper.setPropertyValue(path.toString(), BLANK);
|
||||
} else {
|
||||
String next = path.prefix(index + 1);
|
||||
if (wrapper.getPropertyValue(next) == null) {
|
||||
wrapper.setPropertyValue(next, new LinkedHashMap<String, Object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
return initializePath(wrapper, path, index);
|
||||
}
|
||||
|
||||
private boolean isMapValueStringType(TypeDescriptor descriptor) {
|
||||
if (descriptor == null || descriptor.getMapValueTypeDescriptor() == null) {
|
||||
return false;
|
||||
}
|
||||
if (Properties.class.isAssignableFrom(descriptor.getObjectType())) {
|
||||
// Properties is declared as Map<Object,Object> but we know it's really
|
||||
// Map<String,String>
|
||||
return true;
|
||||
}
|
||||
Class<?> valueType = descriptor.getMapValueTypeDescriptor().getObjectType();
|
||||
return (valueType != null && CharSequence.class.isAssignableFrom(valueType));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private boolean isBlanked(BeanWrapper wrapper, String propertyName, String key) {
|
||||
Object value = (wrapper.isReadableProperty(propertyName) ? wrapper.getPropertyValue(propertyName) : null);
|
||||
if (value instanceof Map) {
|
||||
if (((Map) value).get(key) == BLANK) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void extendCollectionIfNecessary(BeanWrapper wrapper, RelaxedDataBinder.BeanPath path, int index) {
|
||||
String name = path.prefix(index);
|
||||
TypeDescriptor elementDescriptor = wrapper.getPropertyTypeDescriptor(name).getElementTypeDescriptor();
|
||||
if (!elementDescriptor.isMap() && !elementDescriptor.isCollection()
|
||||
&& !elementDescriptor.getType().equals(Object.class)) {
|
||||
return;
|
||||
}
|
||||
Object extend = new LinkedHashMap<String, Object>();
|
||||
if (!elementDescriptor.isMap() && path.isArrayIndex(index)) {
|
||||
extend = new ArrayList<Object>();
|
||||
}
|
||||
wrapper.setPropertyValue(path.prefix(index + 1), extend);
|
||||
}
|
||||
|
||||
private void extendMapIfNecessary(BeanWrapper wrapper, RelaxedDataBinder.BeanPath path, int index) {
|
||||
String name = path.prefix(index);
|
||||
TypeDescriptor parent = wrapper.getPropertyTypeDescriptor(name);
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
TypeDescriptor descriptor = parent.getMapValueTypeDescriptor();
|
||||
if (descriptor == null) {
|
||||
descriptor = TypeDescriptor.valueOf(Object.class);
|
||||
}
|
||||
if (!descriptor.isMap() && !descriptor.isCollection() && !descriptor.getType().equals(Object.class)) {
|
||||
return;
|
||||
}
|
||||
String extensionName = path.prefix(index + 1);
|
||||
if (wrapper.isReadableProperty(extensionName)) {
|
||||
Object currentValue = wrapper.getPropertyValue(extensionName);
|
||||
if ((descriptor.isCollection() && currentValue instanceof Collection)
|
||||
|| (!descriptor.isCollection() && currentValue instanceof Map)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Object extend = new LinkedHashMap<String, Object>();
|
||||
if (descriptor.isCollection()) {
|
||||
extend = new ArrayList<Object>();
|
||||
}
|
||||
if (descriptor.getType().equals(Object.class) && path.isLastNode(index)) {
|
||||
extend = BLANK;
|
||||
}
|
||||
wrapper.setPropertyValue(extensionName, extend);
|
||||
}
|
||||
|
||||
private String getActualPropertyName(BeanWrapper target, String prefix, String name) {
|
||||
String propertyName = resolvePropertyName(target, prefix, name);
|
||||
if (propertyName == null) {
|
||||
propertyName = resolveNestedPropertyName(target, prefix, name);
|
||||
}
|
||||
return (propertyName != null ? propertyName : name);
|
||||
}
|
||||
|
||||
private String resolveNestedPropertyName(BeanWrapper target, String prefix, String name) {
|
||||
StringBuilder candidate = new StringBuilder();
|
||||
for (String field : name.split("[_\\-\\.]")) {
|
||||
candidate.append(candidate.length() > 0 ? "." : "");
|
||||
candidate.append(field);
|
||||
String nested = resolvePropertyName(target, prefix, candidate.toString());
|
||||
if (nested != null) {
|
||||
Class<?> type = target.getPropertyType(nested);
|
||||
if ((type != null) && Map.class.isAssignableFrom(type)) {
|
||||
// Special case for map property (gh-3836).
|
||||
return nested + "[" + name.substring(candidate.length() + 1) + "]";
|
||||
}
|
||||
String propertyName = resolvePropertyName(target,
|
||||
joinString(prefix, nested),
|
||||
name.substring(candidate.length() + 1));
|
||||
if (propertyName != null) {
|
||||
return joinString(nested, propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolvePropertyName(BeanWrapper target, String prefix, String name) {
|
||||
Iterable<String> names = getNameAndAliases(name);
|
||||
for (String nameOrAlias : names) {
|
||||
for (String candidate : new RelaxedNames(nameOrAlias)) {
|
||||
try {
|
||||
if (target.getPropertyType(joinString(prefix, candidate)) != null) {
|
||||
return candidate;
|
||||
}
|
||||
} catch (InvalidPropertyException ex) {
|
||||
// swallow and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String joinString(String prefix, String name) {
|
||||
return (StringUtils.hasLength(prefix) ? prefix + "." + name : name);
|
||||
}
|
||||
|
||||
private Iterable<String> getNameAndAliases(String name) {
|
||||
List<String> aliases = this.nameAliases.get(name);
|
||||
if (aliases == null) {
|
||||
return Collections.singleton(name);
|
||||
}
|
||||
List<String> nameAndAliases = new ArrayList<String>(aliases.size() + 1);
|
||||
nameAndAliases.add(name);
|
||||
nameAndAliases.addAll(aliases);
|
||||
return nameAndAliases;
|
||||
}
|
||||
|
||||
private static Object wrapTarget(Object target) {
|
||||
if (target instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) target;
|
||||
target = new RelaxedDataBinder.MapHolder(map);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
|
||||
if (propertyEditor == null || !EXCLUDED_EDITORS.contains(propertyEditor.getClass())) {
|
||||
super.registerCustomEditor(requiredType, propertyEditor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCustomEditor(Class<?> requiredType, String field, PropertyEditor propertyEditor) {
|
||||
if (propertyEditor == null || !EXCLUDED_EDITORS.contains(propertyEditor.getClass())) {
|
||||
super.registerCustomEditor(requiredType, field, propertyEditor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder to allow Map targets to be bound.
|
||||
*/
|
||||
static class MapHolder {
|
||||
|
||||
private Map<String, Object> map;
|
||||
|
||||
MapHolder(Map<String, Object> map){
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
public void setMap(Map<String, Object> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
public Map<String, Object> getMap() {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A path though properties of a bean.
|
||||
*/
|
||||
private static class BeanPath {
|
||||
|
||||
private List<PathNode> nodes;
|
||||
|
||||
BeanPath(String path){
|
||||
this.nodes = splitPath(path);
|
||||
}
|
||||
|
||||
public List<String> prefixes() {
|
||||
List<String> prefixes = new ArrayList<String>();
|
||||
for (int index = 1; index < this.nodes.size(); index++) {
|
||||
prefixes.add(prefix(index));
|
||||
}
|
||||
return prefixes;
|
||||
}
|
||||
|
||||
public boolean isLastNode(int index) {
|
||||
return index >= this.nodes.size() - 1;
|
||||
}
|
||||
|
||||
private List<PathNode> splitPath(String path) {
|
||||
List<PathNode> nodes = new ArrayList<PathNode>();
|
||||
String current = extractIndexedPaths(path, nodes);
|
||||
for (String name : StringUtils.delimitedListToStringArray(current, ".")) {
|
||||
if (StringUtils.hasText(name)) {
|
||||
nodes.add(new RelaxedDataBinder.BeanPath.PropertyNode(name));
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private String extractIndexedPaths(String path, List<PathNode> nodes) {
|
||||
int startRef = path.indexOf("[");
|
||||
String current = path;
|
||||
while (startRef >= 0) {
|
||||
if (startRef > 0) {
|
||||
nodes.addAll(splitPath(current.substring(0, startRef)));
|
||||
}
|
||||
int endRef = current.indexOf("]", startRef);
|
||||
if (endRef > 0) {
|
||||
String sub = current.substring(startRef + 1, endRef);
|
||||
if (sub.matches("[0-9]+")) {
|
||||
nodes.add(new RelaxedDataBinder.BeanPath.ArrayIndexNode(sub));
|
||||
} else {
|
||||
nodes.add(new RelaxedDataBinder.BeanPath.MapIndexNode(sub));
|
||||
}
|
||||
}
|
||||
current = current.substring(endRef + 1);
|
||||
startRef = current.indexOf("[");
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public void collapseKeys(int index) {
|
||||
List<PathNode> revised = new ArrayList<PathNode>();
|
||||
for (int i = 0; i < index; i++) {
|
||||
revised.add(this.nodes.get(i));
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = index; i < this.nodes.size(); i++) {
|
||||
if (i > index) {
|
||||
builder.append(".");
|
||||
}
|
||||
builder.append(this.nodes.get(i).name);
|
||||
}
|
||||
revised.add(new RelaxedDataBinder.BeanPath.PropertyNode(builder.toString()));
|
||||
this.nodes = revised;
|
||||
}
|
||||
|
||||
public void mapIndex(int index) {
|
||||
RelaxedDataBinder.BeanPath.PathNode node = this.nodes.get(index);
|
||||
if (node instanceof RelaxedDataBinder.BeanPath.PropertyNode) {
|
||||
node = ((RelaxedDataBinder.BeanPath.PropertyNode) node).mapIndex();
|
||||
}
|
||||
this.nodes.set(index, node);
|
||||
}
|
||||
|
||||
public String prefix(int index) {
|
||||
return range(0, index);
|
||||
}
|
||||
|
||||
public void rename(int index, String name) {
|
||||
this.nodes.get(index).name = name;
|
||||
}
|
||||
|
||||
public String name(int index) {
|
||||
if (index < this.nodes.size()) {
|
||||
return this.nodes.get(index).name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String range(int start, int end) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = start; i < end; i++) {
|
||||
RelaxedDataBinder.BeanPath.PathNode node = this.nodes.get(i);
|
||||
builder.append(node);
|
||||
}
|
||||
if (builder.toString().startsWith(("."))) {
|
||||
builder.replace(0, 1, "");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public boolean isArrayIndex(int index) {
|
||||
return this.nodes.get(index) instanceof RelaxedDataBinder.BeanPath.ArrayIndexNode;
|
||||
}
|
||||
|
||||
public boolean isProperty(int index) {
|
||||
return this.nodes.get(index) instanceof RelaxedDataBinder.BeanPath.PropertyNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return prefix(this.nodes.size());
|
||||
}
|
||||
|
||||
private static class PathNode {
|
||||
|
||||
protected String name;
|
||||
|
||||
PathNode(String name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ArrayIndexNode extends RelaxedDataBinder.BeanPath.PathNode {
|
||||
|
||||
ArrayIndexNode(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + this.name + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MapIndexNode extends RelaxedDataBinder.BeanPath.PathNode {
|
||||
|
||||
MapIndexNode(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + this.name + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class PropertyNode extends RelaxedDataBinder.BeanPath.PathNode {
|
||||
|
||||
PropertyNode(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
public RelaxedDataBinder.BeanPath.MapIndexNode mapIndex() {
|
||||
return new RelaxedDataBinder.BeanPath.MapIndexNode(this.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "." + this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended version of {@link BeanPropertyBindingResult} to support relaxed
|
||||
* binding.
|
||||
*/
|
||||
private static class RelaxedBeanPropertyBindingResult extends BeanPropertyBindingResult {
|
||||
|
||||
private RelaxedConversionService conversionService;
|
||||
|
||||
RelaxedBeanPropertyBindingResult(Object target, String objectName, boolean autoGrowNestedPaths,
|
||||
int autoGrowCollectionLimit, ConversionService conversionService){
|
||||
super(target, objectName, autoGrowNestedPaths, autoGrowCollectionLimit);
|
||||
this.conversionService = new RelaxedConversionService(conversionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BeanWrapper createBeanWrapper() {
|
||||
BeanWrapper beanWrapper = new RelaxedDataBinder.RelaxedBeanWrapper(getTarget());
|
||||
beanWrapper.setConversionService(this.conversionService);
|
||||
beanWrapper.registerCustomEditor(InetAddress.class, new InetAddressEditor());
|
||||
return beanWrapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended version of {@link BeanWrapperImpl} to support relaxed binding.
|
||||
*/
|
||||
private static class RelaxedBeanWrapper extends BeanWrapperImpl {
|
||||
|
||||
private static final Set<String> BENIGN_PROPERTY_SOURCE_NAMES;
|
||||
|
||||
static {
|
||||
Set<String> names = new HashSet<String>();
|
||||
names.add(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
|
||||
names.add(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
|
||||
BENIGN_PROPERTY_SOURCE_NAMES = Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
RelaxedBeanWrapper(Object target){
|
||||
super(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPropertyValue(PropertyValue pv) throws BeansException {
|
||||
try {
|
||||
super.setPropertyValue(pv);
|
||||
} catch (NotWritablePropertyException ex) {
|
||||
PropertyOrigin origin = OriginCapablePropertyValue.getOrigin(pv);
|
||||
if (isBenign(origin)) {
|
||||
logger.debug("Ignoring benign property binding failure", ex);
|
||||
return;
|
||||
}
|
||||
if (origin == null) {
|
||||
throw ex;
|
||||
}
|
||||
throw new RelaxedBindingNotWritablePropertyException(ex, origin);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBenign(PropertyOrigin origin) {
|
||||
String name = (origin != null ? origin.getSource().getName() : null);
|
||||
return BENIGN_PROPERTY_SOURCE_NAMES.contains(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class RelaxedBindingNotWritablePropertyException extends NotWritablePropertyException {
|
||||
|
||||
private final String message;
|
||||
|
||||
private final PropertyOrigin propertyOrigin;
|
||||
|
||||
RelaxedBindingNotWritablePropertyException(NotWritablePropertyException ex, PropertyOrigin propertyOrigin){
|
||||
super(ex.getBeanClass(), ex.getPropertyName());
|
||||
this.propertyOrigin = propertyOrigin;
|
||||
this.message = "Failed to bind '" + propertyOrigin.getName() + "' from '"
|
||||
+ propertyOrigin.getSource().getName() + "' to '" + ex.getPropertyName() + "' property on '"
|
||||
+ ex.getBeanClass().getName() + "'";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public PropertyOrigin getPropertyOrigin() {
|
||||
return this.propertyOrigin;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Generates relaxed name variations from a given source.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @see RelaxedDataBinder
|
||||
*/
|
||||
public final class RelaxedNames implements Iterable<String> {
|
||||
|
||||
private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])");
|
||||
|
||||
private static final Pattern SEPARATED_TO_CAMEL_CASE_PATTERN = Pattern.compile("[_\\-.]");
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Set<String> values = new LinkedHashSet<String>();
|
||||
|
||||
/**
|
||||
* Create a new {@link RelaxedNames} instance.
|
||||
*
|
||||
* @param name the source name. For the maximum number of variations specify the
|
||||
* name using dashed notation (e.g. {@literal my-property-name}
|
||||
*/
|
||||
public RelaxedNames(String name){
|
||||
this.name = (name != null ? name : "");
|
||||
initialize(RelaxedNames.this.name, this.values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return this.values.iterator();
|
||||
}
|
||||
|
||||
private void initialize(String name, Set<String> values) {
|
||||
if (values.contains(name)) {
|
||||
return;
|
||||
}
|
||||
for (RelaxedNames.Variation variation : RelaxedNames.Variation.values()) {
|
||||
for (RelaxedNames.Manipulation manipulation : RelaxedNames.Manipulation.values()) {
|
||||
String result = name;
|
||||
result = manipulation.apply(result);
|
||||
result = variation.apply(result);
|
||||
values.add(result);
|
||||
initialize(result, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name variations.
|
||||
*/
|
||||
enum Variation {
|
||||
|
||||
NONE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
LOWERCASE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return (value.isEmpty() ? value : value.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
UPPERCASE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return (value.isEmpty() ? value : value.toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public abstract String apply(String value);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Name manipulations.
|
||||
*/
|
||||
enum Manipulation {
|
||||
|
||||
NONE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
HYPHEN_TO_UNDERSCORE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return (value.indexOf('-') != -1 ? value.replace('-', '_') : value);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
UNDERSCORE_TO_PERIOD {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return (value.indexOf('_') != -1 ? value.replace('_', '.') : value);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
PERIOD_TO_UNDERSCORE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return (value.indexOf('.') != -1 ? value.replace('.', '_') : value);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
CAMELCASE_TO_UNDERSCORE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
if (value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
Matcher matcher = CAMEL_CASE_PATTERN.matcher(value);
|
||||
if (!matcher.find()) {
|
||||
return value;
|
||||
}
|
||||
matcher = matcher.reset();
|
||||
StringBuffer result = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
matcher.appendReplacement(result,
|
||||
matcher.group(1) + '_' + StringUtils.uncapitalize(matcher.group(2)));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
CAMELCASE_TO_HYPHEN {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
if (value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
Matcher matcher = CAMEL_CASE_PATTERN.matcher(value);
|
||||
if (!matcher.find()) {
|
||||
return value;
|
||||
}
|
||||
matcher = matcher.reset();
|
||||
StringBuffer result = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
matcher.appendReplacement(result,
|
||||
matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2)));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
SEPARATED_TO_CAMELCASE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return separatedToCamelCase(value, false);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
CASE_INSENSITIVE_SEPARATED_TO_CAMELCASE {
|
||||
|
||||
@Override
|
||||
public String apply(String value) {
|
||||
return separatedToCamelCase(value, true);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final char[] SUFFIXES = new char[] { '_', '-', '.' };
|
||||
|
||||
public abstract String apply(String value);
|
||||
|
||||
private static String separatedToCamelCase(String value, boolean caseInsensitive) {
|
||||
if (value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String field : SEPARATED_TO_CAMEL_CASE_PATTERN.split(value)) {
|
||||
field = (caseInsensitive ? field.toLowerCase(Locale.ENGLISH) : field);
|
||||
builder.append(builder.length() != 0 ? StringUtils.capitalize(field) : field);
|
||||
}
|
||||
char lastChar = value.charAt(value.length() - 1);
|
||||
for (char suffix : SUFFIXES) {
|
||||
if (lastChar == suffix) {
|
||||
builder.append(suffix);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link RelaxedNames} for the given source camelCase source name.
|
||||
*
|
||||
* @param name the source name in camelCase
|
||||
* @return the relaxed names
|
||||
*/
|
||||
public static RelaxedNames forCamelCase(String name) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char c : name.toCharArray()) {
|
||||
result.append(Character.isUpperCase(c) && result.length() > 0
|
||||
&& result.charAt(result.length() - 1) != '-' ? "-" + Character.toLowerCase(c) : c);
|
||||
}
|
||||
return new RelaxedNames(result.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.bind;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
/**
|
||||
* Converts a String to a Char Array.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StringToCharArrayConverter implements Converter<String, char[]> {
|
||||
|
||||
@Override
|
||||
public char[] convert(String source) {
|
||||
return source.toCharArray();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
|
||||
/**
|
||||
* Convenience base class for {@link Resource} implementations, pre-implementing
|
||||
* typical behavior.
|
||||
* <p>
|
||||
* The "exists" method will check whether a File or InputStream can be opened;
|
||||
* "isOpen" will always return false; "getURL" and "getFile" throw an exception;
|
||||
* and "toString" will return the description.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 28.12.2003
|
||||
*/
|
||||
public abstract class AbstractResource implements Resource {
|
||||
|
||||
/**
|
||||
* This implementation checks whether a File can be opened, falling back to
|
||||
* whether an InputStream can be opened. This will cover both directories and
|
||||
* content resources.
|
||||
*/
|
||||
@Override
|
||||
public boolean exists() {
|
||||
// Try file existence: can we find the file in the file system?
|
||||
try {
|
||||
return getFile().exists();
|
||||
} catch (IOException ex) {
|
||||
// Fall back to stream existence: can we open the stream?
|
||||
try {
|
||||
InputStream is = getInputStream();
|
||||
is.close();
|
||||
return true;
|
||||
} catch (Throwable isEx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation always returns {@code true}.
|
||||
*/
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation always returns {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation throws a FileNotFoundException, assuming that the
|
||||
* resource cannot be resolved to a URL.
|
||||
*/
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation builds a URI based on the URL returned by
|
||||
* {@link #getURL()}.
|
||||
*/
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
URL url = getURL();
|
||||
try {
|
||||
return ResourceUtils.toURI(url);
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new RuntimeException("Invalid URI [" + url + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation throws a FileNotFoundException, assuming that the
|
||||
* resource cannot be resolved to an absolute file path.
|
||||
*/
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation reads the entire InputStream to calculate the content
|
||||
* length. Subclasses will almost always be able to provide a more optimal
|
||||
* version of this, e.g. checking a File length.
|
||||
*
|
||||
* @see #getInputStream()
|
||||
*/
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
InputStream is = getInputStream();
|
||||
Assert.state(is != null, "Resource InputStream must not be null");
|
||||
try {
|
||||
long size = 0;
|
||||
byte[] buf = new byte[255];
|
||||
int read;
|
||||
while ((read = is.read(buf)) != -1) {
|
||||
size += read;
|
||||
}
|
||||
return size;
|
||||
} finally {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation checks the timestamp of the underlying File, if
|
||||
* available.
|
||||
*
|
||||
* @see #getFileForLastModifiedCheck()
|
||||
*/
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
long lastModified = getFileForLastModifiedCheck().lastModified();
|
||||
if (lastModified == 0L) {
|
||||
throw new FileNotFoundException(
|
||||
getDescription() + " cannot be resolved in the file system for resolving its last-modified timestamp");
|
||||
}
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the File to use for timestamp checking.
|
||||
* <p>
|
||||
* The default implementation delegates to {@link #getFile()}.
|
||||
*
|
||||
* @return the File to use for timestamp checking (never {@code null})
|
||||
* @throws FileNotFoundException if the resource cannot be resolved as an
|
||||
* absolute file path, i.e. is not available in a file system
|
||||
* @throws IOException in case of general resolution/reading failures
|
||||
*/
|
||||
protected File getFileForLastModifiedCheck() throws IOException {
|
||||
return getFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation throws a FileNotFoundException, assuming that relative
|
||||
* resources cannot be created for this resource.
|
||||
*/
|
||||
@Override
|
||||
public org.springframework.core.io.Resource createRelative(String relativePath) throws IOException {
|
||||
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation always returns {@code null}, assuming that this resource
|
||||
* type does not have a filename.
|
||||
*/
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns the description of this resource.
|
||||
*
|
||||
* @see #getDescription()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation compares description strings.
|
||||
*
|
||||
* @see #getDescription()
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return (obj == this
|
||||
|| (obj instanceof org.springframework.core.io.Resource
|
||||
&& ((org.springframework.core.io.Resource) obj).getDescription().equals(getDescription())));
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns the description's hash code.
|
||||
*
|
||||
* @see #getDescription()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getDescription().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Resource} implementation for a given byte array.
|
||||
* <p>
|
||||
* Creates a {@link ByteArrayInputStream} for the given byte array.
|
||||
* <p>
|
||||
* Useful for loading content from any given byte array, without having to
|
||||
* resort to a single-use {@link InputStreamResource}. Particularly useful for
|
||||
* creating mail attachments from local content, where JavaMail needs to be able
|
||||
* to read the stream multiple times.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @see ByteArrayInputStream
|
||||
* @see InputStreamResource
|
||||
* @since 1.2.3
|
||||
*/
|
||||
public class ByteArrayResource extends AbstractResource {
|
||||
|
||||
private final byte[] byteArray;
|
||||
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* Create a new {@code ByteArrayResource}.
|
||||
*
|
||||
* @param byteArray the byte array to wrap
|
||||
*/
|
||||
public ByteArrayResource(byte[] byteArray){
|
||||
this(byteArray, "resource loaded from byte array");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code ByteArrayResource} with a description.
|
||||
*
|
||||
* @param byteArray the byte array to wrap
|
||||
* @param description where the byte array comes from
|
||||
*/
|
||||
public ByteArrayResource(byte[] byteArray, String description){
|
||||
Assert.notNull(byteArray, "Byte array must not be null");
|
||||
this.byteArray = byteArray;
|
||||
this.description = (description != null ? description : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying byte array.
|
||||
*/
|
||||
public final byte[] getByteArray() {
|
||||
return this.byteArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation always returns {@code true}.
|
||||
*/
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns the length of the underlying byte array.
|
||||
*/
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return this.byteArray.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns a ByteArrayInputStream for the underlying byte
|
||||
* array.
|
||||
*
|
||||
* @see ByteArrayInputStream
|
||||
*/
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(this.byteArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns a description that includes the passed-in
|
||||
* {@code description}, if any.
|
||||
*/
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Byte array resource [" + this.description + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation compares the underlying byte array.
|
||||
*
|
||||
* @see Arrays#equals(byte[], byte[])
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return (obj == this || (obj instanceof org.springframework.core.io.ByteArrayResource
|
||||
&& Arrays.equals(((ByteArrayResource) obj).byteArray, this.byteArray)));
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns the hash code based on the underlying byte array.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (byte[].class.hashCode() * 29 * this.byteArray.length);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Composite {@link PropertySource} implementation that iterates over a set of
|
||||
* {@link PropertySource} instances. Necessary in cases where multiple property
|
||||
* sources share the same name, e.g. when multiple values are supplied to
|
||||
* {@code @PropertySource}.
|
||||
* <p>
|
||||
* As of Spring 4.1.2, this class extends {@link EnumerablePropertySource}
|
||||
* instead of plain {@link PropertySource}, exposing {@link #getPropertyNames()}
|
||||
* based on the accumulated property names from all contained sources (as far as
|
||||
* possible).
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @author Phillip Webb
|
||||
* @since 3.1.1
|
||||
*/
|
||||
public class CompositePropertySource extends EnumerablePropertySource<Object> {
|
||||
|
||||
private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>();
|
||||
|
||||
/**
|
||||
* Create a new {@code CompositePropertySource}.
|
||||
*
|
||||
* @param name the name of the property source
|
||||
*/
|
||||
public CompositePropertySource(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
for (PropertySource<?> propertySource : this.propertySources) {
|
||||
Object candidate = propertySource.getProperty(name);
|
||||
if (candidate != null) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsProperty(String name) {
|
||||
for (PropertySource<?> propertySource : this.propertySources) {
|
||||
if (propertySource.containsProperty(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getPropertyNames() {
|
||||
Set<String> names = new LinkedHashSet<String>();
|
||||
for (PropertySource<?> propertySource : this.propertySources) {
|
||||
if (!(propertySource instanceof EnumerablePropertySource)) {
|
||||
throw new IllegalStateException(
|
||||
"Failed to enumerate property names due to non-enumerable property source: " + propertySource);
|
||||
}
|
||||
names.addAll(Arrays.asList(((EnumerablePropertySource<?>) propertySource).getPropertyNames()));
|
||||
}
|
||||
return StringUtils.toStringArray(names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link PropertySource} to the end of the chain.
|
||||
*
|
||||
* @param propertySource the PropertySource to add
|
||||
*/
|
||||
public void addPropertySource(PropertySource<?> propertySource) {
|
||||
this.propertySources.add(propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link PropertySource} to the start of the chain.
|
||||
*
|
||||
* @param propertySource the PropertySource to add
|
||||
* @since 4.1
|
||||
*/
|
||||
public void addFirstPropertySource(PropertySource<?> propertySource) {
|
||||
List<PropertySource<?>> existing = new ArrayList<PropertySource<?>>(this.propertySources);
|
||||
this.propertySources.clear();
|
||||
this.propertySources.add(propertySource);
|
||||
this.propertySources.addAll(existing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all property sources that this composite source holds.
|
||||
*
|
||||
* @since 4.1.1
|
||||
*/
|
||||
public Collection<PropertySource<?>> getPropertySources() {
|
||||
return this.propertySources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String
|
||||
.format("%s [name='%s', propertySources=%s]", getClass().getSimpleName(), this.name, this.propertySources);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A {@link PropertySource} implementation capable of interrogating its
|
||||
* underlying source object to enumerate all possible property name/value pairs.
|
||||
* Exposes the {@link #getPropertyNames()} method to allow callers to introspect
|
||||
* available properties without having to access the underlying source object.
|
||||
* This also facilitates a more efficient implementation of
|
||||
* {@link #containsProperty(String)}, in that it can call
|
||||
* {@link #getPropertyNames()} and iterate through the returned array rather
|
||||
* than attempting a call to {@link #getProperty(String)} which may be more
|
||||
* expensive. Implementations may consider caching the result of
|
||||
* {@link #getPropertyNames()} to fully exploit this performance opportunity.
|
||||
* <p>
|
||||
* Most framework-provided {@code PropertySource} implementations are
|
||||
* enumerable; a counter-example would be {@code JndiPropertySource} where, due
|
||||
* to the nature of JNDI it is not possible to determine all possible property
|
||||
* names at any given time; rather it is only possible to try to access a
|
||||
* property (via {@link #getProperty(String)}) in order to evaluate whether it
|
||||
* is present or not.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
*/
|
||||
public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
|
||||
|
||||
public EnumerablePropertySource(String name, T source){
|
||||
super(name, source);
|
||||
}
|
||||
|
||||
protected EnumerablePropertySource(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this {@code PropertySource} contains a property with the given
|
||||
* name.
|
||||
* <p>
|
||||
* This implementation checks for the presence of the given name within the
|
||||
* {@link #getPropertyNames()} array.
|
||||
*
|
||||
* @param name the name of the property to find
|
||||
*/
|
||||
@Override
|
||||
public boolean containsProperty(String name) {
|
||||
return ObjectUtils.containsElement(getPropertyNames(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the names of all properties contained by the {@linkplain #getSource()
|
||||
* source} object (never {@code null}).
|
||||
*/
|
||||
public abstract String[] getPropertyNames();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link PropertySource} that reads keys and values from a {@code Map} object.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
* @see PropertiesPropertySource
|
||||
*/
|
||||
public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> {
|
||||
|
||||
public MapPropertySource(String name, Map<String, Object> source){
|
||||
super(name, source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
return this.source.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsProperty(String name) {
|
||||
return this.source.containsKey(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getPropertyNames() {
|
||||
return StringUtils.toStringArray(this.source.keySet());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.env.PropertyResolver;
|
||||
import org.springframework.core.env.PropertySourcesPropertyResolver;
|
||||
|
||||
/**
|
||||
* Default implementation of the {@link PropertySources} interface. Allows
|
||||
* manipulation of contained property sources and provides a constructor for
|
||||
* copying an existing {@code PropertySources} instance.
|
||||
* <p>
|
||||
* Where <em>precedence</em> is mentioned in methods such as {@link #addFirst}
|
||||
* and {@link #addLast}, this is with regard to the order in which property
|
||||
* sources will be searched when resolving a given property with a
|
||||
* {@link PropertyResolver}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @see PropertySourcesPropertyResolver
|
||||
* @since 3.1
|
||||
*/
|
||||
public class MutablePropertySources implements PropertySources {
|
||||
|
||||
private final Log logger;
|
||||
|
||||
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();
|
||||
|
||||
/**
|
||||
* Create a new {@link MutablePropertySources}
|
||||
* object.
|
||||
*/
|
||||
public MutablePropertySources(){
|
||||
this.logger = LogFactory.getLog(getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code MutablePropertySources} from the given propertySources
|
||||
* object, preserving the original order of contained {@code PropertySource}
|
||||
* objects.
|
||||
*/
|
||||
public MutablePropertySources(PropertySources propertySources){
|
||||
this();
|
||||
for (PropertySource<?> propertySource : propertySources) {
|
||||
addLast(propertySource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MutablePropertySources}
|
||||
* object and inherit the given logger, usually from an enclosing
|
||||
* {@link Environment}.
|
||||
*/
|
||||
MutablePropertySources(Log logger){
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String name) {
|
||||
return this.propertySourceList.contains(PropertySource.named(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertySource<?> get(String name) {
|
||||
int index = this.propertySourceList.indexOf(PropertySource.named(name));
|
||||
return (index != -1 ? this.propertySourceList.get(index) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PropertySource<?>> iterator() {
|
||||
return this.propertySourceList.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given property source object with highest precedence.
|
||||
*/
|
||||
public void addFirst(PropertySource<?> propertySource) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Adding PropertySource '" + propertySource.getName() + "' with highest search precedence");
|
||||
}
|
||||
removeIfPresent(propertySource);
|
||||
this.propertySourceList.add(0, propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given property source object with lowest precedence.
|
||||
*/
|
||||
public void addLast(PropertySource<?> propertySource) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Adding PropertySource '" + propertySource.getName() + "' with lowest search precedence");
|
||||
}
|
||||
removeIfPresent(propertySource);
|
||||
this.propertySourceList.add(propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given property source object with precedence immediately higher than
|
||||
* the named relative property source.
|
||||
*/
|
||||
public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Adding PropertySource '" + propertySource.getName()
|
||||
+ "' with search precedence immediately higher than '" + relativePropertySourceName + "'");
|
||||
}
|
||||
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
|
||||
removeIfPresent(propertySource);
|
||||
int index = assertPresentAndGetIndex(relativePropertySourceName);
|
||||
addAtIndex(index, propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given property source object with precedence immediately lower than
|
||||
* the named relative property source.
|
||||
*/
|
||||
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Adding PropertySource '" + propertySource.getName()
|
||||
+ "' with search precedence immediately lower than '" + relativePropertySourceName + "'");
|
||||
}
|
||||
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
|
||||
removeIfPresent(propertySource);
|
||||
int index = assertPresentAndGetIndex(relativePropertySourceName);
|
||||
addAtIndex(index + 1, propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the precedence of the given property source, {@code -1} if not found.
|
||||
*/
|
||||
public int precedenceOf(PropertySource<?> propertySource) {
|
||||
return this.propertySourceList.indexOf(propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and return the property source with the given name, {@code null} if
|
||||
* not found.
|
||||
*
|
||||
* @param name the name of the property source to find and remove
|
||||
*/
|
||||
public PropertySource<?> remove(String name) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Removing PropertySource '" + name + "'");
|
||||
}
|
||||
int index = this.propertySourceList.indexOf(PropertySource.named(name));
|
||||
return (index != -1 ? this.propertySourceList.remove(index) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the property source with the given name with the given property
|
||||
* source object.
|
||||
*
|
||||
* @param name the name of the property source to find and replace
|
||||
* @param propertySource the replacement property source
|
||||
* @throws IllegalArgumentException if no property source with the given name is
|
||||
* present
|
||||
* @see #contains
|
||||
*/
|
||||
public void replace(String name, PropertySource<?> propertySource) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Replacing PropertySource '" + name + "' with '" + propertySource.getName() + "'");
|
||||
}
|
||||
int index = assertPresentAndGetIndex(name);
|
||||
this.propertySourceList.set(index, propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of {@link PropertySource} objects contained.
|
||||
*/
|
||||
public int size() {
|
||||
return this.propertySourceList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.propertySourceList.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given property source is not being added relative to itself.
|
||||
*/
|
||||
protected void assertLegalRelativeAddition(String relativePropertySourceName, PropertySource<?> propertySource) {
|
||||
String newPropertySourceName = propertySource.getName();
|
||||
if (relativePropertySourceName.equals(newPropertySourceName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"PropertySource named '" + newPropertySourceName + "' cannot be added relative to itself");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given property source if it is present.
|
||||
*/
|
||||
protected void removeIfPresent(PropertySource<?> propertySource) {
|
||||
this.propertySourceList.remove(propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given property source at a particular index in the list.
|
||||
*/
|
||||
private void addAtIndex(int index, PropertySource<?> propertySource) {
|
||||
removeIfPresent(propertySource);
|
||||
this.propertySourceList.add(index, propertySource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the named property source is present and return its index.
|
||||
*
|
||||
* @param name {@linkplain PropertySource#getName() name of the property source}
|
||||
* to find
|
||||
* @throws IllegalArgumentException if the named property source is not present
|
||||
*/
|
||||
private int assertPresentAndGetIndex(String name) {
|
||||
int index = this.propertySourceList.indexOf(PropertySource.named(name));
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist");
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
/**
|
||||
* {@link PropertySource} implementation that extracts properties from a
|
||||
* {@link Properties} object.
|
||||
* <p>
|
||||
* Note that because a {@code Properties} object is technically an
|
||||
* {@code <Object, Object>} {@link java.util.Hashtable Hashtable}, one may
|
||||
* contain non-{@code String} keys or values. This implementation, however is
|
||||
* restricted to accessing only {@code String}-based keys and values, in the
|
||||
* same fashion as {@link Properties#getProperty} and
|
||||
* {@link Properties#setProperty}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
*/
|
||||
public class PropertiesPropertySource extends MapPropertySource {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public PropertiesPropertySource(String name, Properties source){
|
||||
super(name, (Map) source);
|
||||
}
|
||||
|
||||
protected PropertiesPropertySource(String name, Map<String, Object> source){
|
||||
super(name, source);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class representing a source of name/value property pairs. The
|
||||
* underlying {@linkplain #getSource() source object} may be of any type
|
||||
* {@code T} that encapsulates properties. Examples include
|
||||
* {@link java.util.Properties} objects, {@link java.util.Map} objects,
|
||||
* {@code ServletContext} and {@code ServletConfig} objects (for access to init
|
||||
* parameters). Explore the {@code PropertySource} type hierarchy to see
|
||||
* provided implementations.
|
||||
* <p>
|
||||
* {@code PropertySource} objects are not typically used in isolation, but
|
||||
* rather through a {@link PropertySources} object, which aggregates property
|
||||
* sources and in conjunction with a {@link PropertyResolver} implementation
|
||||
* that can perform precedence-based searches across the set of
|
||||
* {@code PropertySources}.
|
||||
* <p>
|
||||
* {@code PropertySource} identity is determined not based on the content of
|
||||
* encapsulated properties, but rather based on the {@link #getName() name} of
|
||||
* the {@code PropertySource} alone. This is useful for manipulating
|
||||
* {@code PropertySource} objects when in collection contexts. See operations in
|
||||
* {@link MutablePropertySources} as well as the {@link #named(String)} and
|
||||
* {@link #toString()} methods for details.
|
||||
* <p>
|
||||
* Note that when working
|
||||
* with @{@link org.springframework.context.annotation.Configuration
|
||||
* Configuration} classes that the @{@link PropertySource PropertySource}
|
||||
* annotation provides a convenient and declarative way of adding property
|
||||
* sources to the enclosing {@code Environment}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @since 3.1
|
||||
* @see PropertySources
|
||||
* @see MutablePropertySources
|
||||
* @see PropertySource
|
||||
*/
|
||||
public abstract class PropertySource<T> {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
protected final String name;
|
||||
|
||||
protected final T source;
|
||||
|
||||
/**
|
||||
* Create a new {@code PropertySource} with the given name and source object.
|
||||
*/
|
||||
public PropertySource(String name, T source){
|
||||
Assert.hasText(name, "Property source name must contain at least one character");
|
||||
Assert.notNull(source, "Property source must not be null");
|
||||
this.name = name;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code PropertySource} with the given name and with a new
|
||||
* {@code Object} instance as the underlying source.
|
||||
* <p>
|
||||
* Often useful in testing scenarios when creating anonymous implementations
|
||||
* that never query an actual source but rather return hard-coded values.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public PropertySource(String name){
|
||||
this(name, (T) new Object());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of this {@code PropertySource}
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying source object for this {@code PropertySource}.
|
||||
*/
|
||||
public T getSource() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this {@code PropertySource} contains the given name.
|
||||
* <p>
|
||||
* This implementation simply checks for a {@code null} return value from
|
||||
* {@link #getProperty(String)}. Subclasses may wish to implement a more
|
||||
* efficient algorithm if possible.
|
||||
*
|
||||
* @param name the property name to find
|
||||
*/
|
||||
public boolean containsProperty(String name) {
|
||||
return (getProperty(name) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value associated with the given name, or {@code null} if not
|
||||
* found.
|
||||
*
|
||||
* @param name the property to find
|
||||
*/
|
||||
public abstract Object getProperty(String name);
|
||||
|
||||
/**
|
||||
* This {@code PropertySource} object is equal to the given object if:
|
||||
* <ul>
|
||||
* <li>they are the same instance
|
||||
* <li>the {@code name} properties for both objects are equal
|
||||
* </ul>
|
||||
* <p>
|
||||
* No properties other than {@code name} are evaluated.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return (this == obj || (obj instanceof PropertySource
|
||||
&& ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash code derived from the {@code name} property of this
|
||||
* {@code PropertySource} object.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return ObjectUtils.nullSafeHashCode(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce concise output (type and name) if the current log level does not
|
||||
* include debug. If debug is enabled, produce verbose output including the hash
|
||||
* code of the PropertySource instance and every name/value property pair.
|
||||
* <p>
|
||||
* This variable verbosity is useful as a property source such as system
|
||||
* properties or environment variables may contain an arbitrary number of
|
||||
* property pairs, potentially leading to difficult to read exception and log
|
||||
* messages.
|
||||
*
|
||||
* @see Log#isDebugEnabled()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (logger.isDebugEnabled()) {
|
||||
return getClass().getSimpleName() + "@" + System.identityHashCode(this) + " {name='" + this.name
|
||||
+ "', properties=" + this.source + "}";
|
||||
} else {
|
||||
return getClass().getSimpleName() + " {name='" + this.name + "'}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code PropertySource} implementation intended for collection
|
||||
* comparison purposes only.
|
||||
* <p>
|
||||
* Primarily for internal use, but given a collection of {@code PropertySource}
|
||||
* objects, may be used as follows:
|
||||
*
|
||||
* <pre class="code">
|
||||
*
|
||||
* {
|
||||
* @code
|
||||
* List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
|
||||
* sources.add(new MapPropertySource("sourceA", mapA));
|
||||
* sources.add(new MapPropertySource("sourceB", mapB));
|
||||
* assert sources.contains(PropertySource.named("sourceA"));
|
||||
* assert sources.contains(PropertySource.named("sourceB"));
|
||||
* assert !sources.contains(PropertySource.named("sourceC"));
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The returned {@code PropertySource} will throw
|
||||
* {@code UnsupportedOperationException} if any methods other than
|
||||
* {@code equals(Object)}, {@code hashCode()}, and {@code toString()} are
|
||||
* called.
|
||||
*
|
||||
* @param name the name of the comparison {@code PropertySource} to be created
|
||||
* and returned.
|
||||
*/
|
||||
public static PropertySource<?> named(String name) {
|
||||
return new ComparisonPropertySource(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code PropertySource} to be used as a placeholder in cases where an actual
|
||||
* property source cannot be eagerly initialized at application context creation
|
||||
* time. For example, a {@code ServletContext}-based property source must wait
|
||||
* until the {@code ServletContext} object is available to its enclosing
|
||||
* {@code ApplicationContext}. In such cases, a stub should be used to hold the
|
||||
* intended default position/order of the property source, then be replaced
|
||||
* during context refresh.
|
||||
*
|
||||
* @see org.springframework.web.context.support.StandardServletEnvironment
|
||||
* @see org.springframework.web.context.support.ServletContextPropertySource
|
||||
*/
|
||||
public static class StubPropertySource extends PropertySource<Object> {
|
||||
|
||||
public StubPropertySource(String name){
|
||||
super(name, new Object());
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns {@code null}.
|
||||
*/
|
||||
@Override
|
||||
public String getProperty(String name) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PropertySource#named(String)
|
||||
*/
|
||||
static class ComparisonPropertySource extends StubPropertySource {
|
||||
|
||||
private static final String USAGE_ERROR = "ComparisonPropertySource instances are for use with collection comparison only";
|
||||
|
||||
public ComparisonPropertySource(String name){
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSource() {
|
||||
throw new UnsupportedOperationException(USAGE_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsProperty(String name) {
|
||||
throw new UnsupportedOperationException(USAGE_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProperty(String name) {
|
||||
throw new UnsupportedOperationException(USAGE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
|
||||
/**
|
||||
* Strategy interface located via {@link SpringFactoriesLoader} and used to load
|
||||
* a {@link PropertySource}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public interface PropertySourceLoader {
|
||||
|
||||
/**
|
||||
* Returns the file extensions that the loader supports (excluding the '.').
|
||||
*
|
||||
* @return the file extensions
|
||||
*/
|
||||
String[] getFileExtensions();
|
||||
|
||||
/**
|
||||
* Load the resource into a property source.
|
||||
*
|
||||
* @param name the name of the property source
|
||||
* @param resource the resource to load
|
||||
* @param profile the name of the profile to load or {@code null}. The profile
|
||||
* can be used to load multi-document files (such as YAML). Simple property
|
||||
* formats should {@code null} when asked to load a profile.
|
||||
* @return a property source or {@code null}
|
||||
* @throws IOException if the source cannot be loaded
|
||||
*/
|
||||
PropertySource<?> load(String name, Resource resource, String profile) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
/**
|
||||
* Holder containing one or more {@link PropertySource} objects.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @since 3.1
|
||||
*/
|
||||
public interface PropertySources extends Iterable<PropertySource<?>> {
|
||||
|
||||
/**
|
||||
* Return whether a property source with the given name is contained.
|
||||
*
|
||||
* @param name the {@linkplain PropertySource#getName() name of the property source} to find
|
||||
*/
|
||||
boolean contains(String name);
|
||||
|
||||
/**
|
||||
* Return the property source with the given name, {@code null} if not found.
|
||||
*
|
||||
* @param name the {@linkplain PropertySource#getName() name of the property source} to find
|
||||
*/
|
||||
PropertySource<?> get(String name);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
import org.springframework.core.io.*;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
|
||||
/**
|
||||
* Interface for a resource descriptor that abstracts from the actual type of
|
||||
* underlying resource, such as a file or class path resource.
|
||||
* <p>
|
||||
* An InputStream can be opened for every resource if it exists in physical
|
||||
* form, but a URL or File handle can just be returned for certain resources.
|
||||
* The actual behavior is implementation-specific.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 28.12.2003
|
||||
* @see #getInputStream()
|
||||
* @see #getURL()
|
||||
* @see #getURI()
|
||||
* @see #getFile()
|
||||
* @see WritableResource
|
||||
* @see ContextResource
|
||||
* @see UrlResource
|
||||
* @see ClassPathResource
|
||||
* @see FileSystemResource
|
||||
* @see PathResource
|
||||
* @see ByteArrayResource
|
||||
* @see InputStreamResource
|
||||
*/
|
||||
public interface Resource extends InputStreamSource {
|
||||
|
||||
boolean exists();
|
||||
|
||||
boolean isReadable();
|
||||
|
||||
boolean isOpen();
|
||||
|
||||
URL getURL() throws IOException;
|
||||
|
||||
URI getURI() throws IOException;
|
||||
|
||||
File getFile() throws IOException;
|
||||
|
||||
long contentLength() throws IOException;
|
||||
|
||||
long lastModified() throws IOException;
|
||||
|
||||
org.springframework.core.io.Resource createRelative(String var1) throws IOException;
|
||||
|
||||
String getFilename();
|
||||
|
||||
String getDescription();
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link YamlProcessor.DocumentMatcher} backed by
|
||||
* {@link Environment#getActiveProfiles()}. A YAML document may define a
|
||||
* "spring.profiles" element as a comma-separated list of Spring profile names,
|
||||
* optionally negated using the {@code !} character. If both negated and
|
||||
* non-negated profiles are specified for a single document, at least one
|
||||
* non-negated profile must match and no negated profiles may match.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Matt Benson
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class SpringProfileDocumentMatcher implements YamlProcessor.DocumentMatcher {
|
||||
|
||||
private String[] activeProfiles = new String[0];
|
||||
|
||||
public SpringProfileDocumentMatcher(){
|
||||
}
|
||||
|
||||
public SpringProfileDocumentMatcher(String... profiles){
|
||||
addActiveProfiles(profiles);
|
||||
}
|
||||
|
||||
public void addActiveProfiles(String... profiles) {
|
||||
LinkedHashSet<String> set = new LinkedHashSet<String>(Arrays.asList(this.activeProfiles));
|
||||
Collections.addAll(set, profiles);
|
||||
this.activeProfiles = set.toArray(new String[set.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public YamlProcessor.MatchStatus matches(Properties properties) {
|
||||
List<String> profiles = extractSpringProfiles(properties);
|
||||
ProfilesMatcher profilesMatcher = getProfilesMatcher();
|
||||
Set<String> negative = extractProfiles(profiles, ProfileType.NEGATIVE);
|
||||
Set<String> positive = extractProfiles(profiles, ProfileType.POSITIVE);
|
||||
if (!CollectionUtils.isEmpty(negative)) {
|
||||
if (profilesMatcher.matches(negative) == YamlProcessor.MatchStatus.FOUND) {
|
||||
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||
}
|
||||
if (CollectionUtils.isEmpty(positive)) {
|
||||
return YamlProcessor.MatchStatus.FOUND;
|
||||
}
|
||||
}
|
||||
return profilesMatcher.matches(positive);
|
||||
}
|
||||
|
||||
private List<String> extractSpringProfiles(Properties properties) {
|
||||
SpringProperties springProperties = new SpringProperties();
|
||||
MutablePropertySources propertySources = new MutablePropertySources();
|
||||
propertySources.addFirst(new PropertiesPropertySource("profiles", properties));
|
||||
// PropertyValues propertyValues = new PropertySourcesPropertyValues(
|
||||
// propertySources);
|
||||
// new RelaxedDataBinder(springProperties, "spring").bind(propertyValues);
|
||||
// TODO
|
||||
List<String> profiles = springProperties.getProfiles();
|
||||
return profiles;
|
||||
}
|
||||
|
||||
private ProfilesMatcher getProfilesMatcher() {
|
||||
return (this.activeProfiles.length != 0 ? new ActiveProfilesMatcher(
|
||||
new HashSet<String>(Arrays.asList(this.activeProfiles))) : new EmptyProfilesMatcher());
|
||||
}
|
||||
|
||||
private Set<String> extractProfiles(List<String> profiles, ProfileType type) {
|
||||
if (CollectionUtils.isEmpty(profiles)) {
|
||||
return null;
|
||||
}
|
||||
Set<String> extractedProfiles = new HashSet<String>();
|
||||
for (String candidate : profiles) {
|
||||
ProfileType candidateType = ProfileType.POSITIVE;
|
||||
if (candidate.startsWith("!")) {
|
||||
candidateType = ProfileType.NEGATIVE;
|
||||
}
|
||||
if (candidateType == type) {
|
||||
extractedProfiles.add(type != ProfileType.POSITIVE ? candidate.substring(1) : candidate);
|
||||
}
|
||||
}
|
||||
return extractedProfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile match types.
|
||||
*/
|
||||
enum ProfileType {
|
||||
|
||||
POSITIVE, NEGATIVE
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for profile matchers.
|
||||
*/
|
||||
private abstract static class ProfilesMatcher {
|
||||
|
||||
public final YamlProcessor.MatchStatus matches(Set<String> profiles) {
|
||||
if (CollectionUtils.isEmpty(profiles)) {
|
||||
return YamlProcessor.MatchStatus.ABSTAIN;
|
||||
}
|
||||
return doMatches(profiles);
|
||||
}
|
||||
|
||||
protected abstract YamlProcessor.MatchStatus doMatches(Set<String> profiles);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ProfilesMatcher} that matches when a value in {@code spring.profiles}
|
||||
* is also in {@code spring.profiles.active}.
|
||||
*/
|
||||
private static class ActiveProfilesMatcher extends ProfilesMatcher {
|
||||
|
||||
private final Set<String> activeProfiles;
|
||||
|
||||
ActiveProfilesMatcher(Set<String> activeProfiles){
|
||||
this.activeProfiles = activeProfiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected YamlProcessor.MatchStatus doMatches(Set<String> profiles) {
|
||||
if (profiles.isEmpty()) {
|
||||
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||
}
|
||||
for (String activeProfile : this.activeProfiles) {
|
||||
if (profiles.contains(activeProfile)) {
|
||||
return YamlProcessor.MatchStatus.FOUND;
|
||||
}
|
||||
}
|
||||
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ProfilesMatcher} that matches when {@code
|
||||
* spring.profiles} is empty or contains a value with no text.
|
||||
*
|
||||
* @see StringUtils#hasText(String)
|
||||
*/
|
||||
private static class EmptyProfilesMatcher extends ProfilesMatcher {
|
||||
|
||||
@Override
|
||||
public YamlProcessor.MatchStatus doMatches(Set<String> springProfiles) {
|
||||
if (springProfiles.isEmpty()) {
|
||||
return YamlProcessor.MatchStatus.FOUND;
|
||||
}
|
||||
for (String profile : springProfiles) {
|
||||
if (!StringUtils.hasText(profile)) {
|
||||
return YamlProcessor.MatchStatus.FOUND;
|
||||
}
|
||||
}
|
||||
return YamlProcessor.MatchStatus.NOT_FOUND;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for binding {@code spring.profiles} property.
|
||||
*/
|
||||
static class SpringProperties {
|
||||
|
||||
private List<String> profiles = new ArrayList<String>();
|
||||
|
||||
public List<String> getProfiles() {
|
||||
return this.profiles;
|
||||
}
|
||||
|
||||
public void setProfiles(List<String> profiles) {
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.Constructor;
|
||||
import org.yaml.snakeyaml.nodes.MappingNode;
|
||||
import org.yaml.snakeyaml.parser.ParserException;
|
||||
import org.yaml.snakeyaml.reader.UnicodeReader;
|
||||
|
||||
/**
|
||||
* Base class for YAML factories.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.1
|
||||
*/
|
||||
public abstract class YamlProcessor {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE;
|
||||
|
||||
private Resource[] resources = new Resource[0];
|
||||
|
||||
private List<DocumentMatcher> documentMatchers = Collections.emptyList();
|
||||
|
||||
private boolean matchDefault = true;
|
||||
|
||||
/**
|
||||
* A map of document matchers allowing callers to selectively use only some of
|
||||
* the documents in a YAML resource. In YAML documents are separated by
|
||||
* <code>---<code> lines, and each document is converted to properties before
|
||||
* the match is made. E.g.
|
||||
*
|
||||
* <pre class="code">
|
||||
* environment: dev
|
||||
* url: http://dev.bar.com
|
||||
* name: Developer Setup
|
||||
* ---
|
||||
* environment: prod
|
||||
* url:http://foo.bar.com
|
||||
* name: My Cool App
|
||||
* </pre>
|
||||
*
|
||||
* when mapped with
|
||||
*
|
||||
* <pre class="code">
|
||||
* setDocumentMatchers(properties -> ("prod"
|
||||
* .equals(properties.getProperty("environment")) ? MatchStatus.FOUND : MatchStatus.NOT_FOUND));
|
||||
* </pre>
|
||||
*
|
||||
* would end up as
|
||||
*
|
||||
* <pre class="code">
|
||||
* environment=prod
|
||||
* url=http://foo.bar.com
|
||||
* name=My Cool App
|
||||
* </pre>
|
||||
*/
|
||||
public void setDocumentMatchers(DocumentMatcher... matchers) {
|
||||
this.documentMatchers = Arrays.asList(matchers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag indicating that a document for which all the
|
||||
* {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain
|
||||
* will nevertheless match. Default is {@code true}.
|
||||
*/
|
||||
public void setMatchDefault(boolean matchDefault) {
|
||||
this.matchDefault = matchDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to use for resolving resources. Each resource will be converted to a
|
||||
* Map, so this property is used to decide which map entries to keep in the
|
||||
* final output from this factory. Default is {@link ResolutionMethod#OVERRIDE}.
|
||||
*/
|
||||
public void setResolutionMethod(ResolutionMethod resolutionMethod) {
|
||||
Assert.notNull(resolutionMethod, "ResolutionMethod must not be null");
|
||||
this.resolutionMethod = resolutionMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set locations of YAML {@link Resource resources} to be loaded.
|
||||
*
|
||||
* @see ResolutionMethod
|
||||
*/
|
||||
public void setResources(Resource... resources) {
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an opportunity for subclasses to process the Yaml parsed from the
|
||||
* supplied resources. Each resource is parsed in turn and the documents inside
|
||||
* checked against the {@link #setDocumentMatchers(DocumentMatcher...)
|
||||
* matchers}. If a document matches it is passed into the callback, along with
|
||||
* its representation as Properties. Depending on the
|
||||
* {@link #setResolutionMethod(ResolutionMethod)} not all of the documents will
|
||||
* be parsed.
|
||||
*
|
||||
* @param callback a callback to delegate to once matching documents are found
|
||||
* @see #createYaml()
|
||||
*/
|
||||
protected void process(MatchCallback callback) {
|
||||
Yaml yaml = createYaml();
|
||||
for (Resource resource : this.resources) {
|
||||
boolean found = process(callback, yaml, resource);
|
||||
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link Yaml} instance to use.
|
||||
*/
|
||||
protected Yaml createYaml() {
|
||||
return new Yaml(new StrictMapAppenderConstructor());
|
||||
}
|
||||
|
||||
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
|
||||
int count = 0;
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loading from YAML: " + resource);
|
||||
}
|
||||
Reader reader = new UnicodeReader(resource.getInputStream());
|
||||
try {
|
||||
for (Object object : yaml.loadAll(reader)) {
|
||||
if (object != null && process(asMap(object), callback)) {
|
||||
count++;
|
||||
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
"Loaded " + count + " document" + (count > 1 ? "s" : "") + " from YAML resource: " + resource);
|
||||
}
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
handleProcessError(resource, ex);
|
||||
}
|
||||
return (count > 0);
|
||||
}
|
||||
|
||||
private void handleProcessError(Resource resource, IOException ex) {
|
||||
if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND
|
||||
&& this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Could not load map from " + resource + ": " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> asMap(Object object) {
|
||||
// YAML can have numbers as keys
|
||||
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||
if (!(object instanceof Map)) {
|
||||
// A document can be a text literal
|
||||
result.put("document", object);
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<Object, Object> map = (Map<Object, Object>) object;
|
||||
for (Map.Entry<Object, Object> entry : map.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map) {
|
||||
value = asMap(value);
|
||||
}
|
||||
Object key = entry.getKey();
|
||||
if (key instanceof CharSequence) {
|
||||
result.put(key.toString(), value);
|
||||
} else {
|
||||
// It has to be a map key in this case
|
||||
result.put("[" + key.toString() + "]", value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean process(Map<String, Object> map, MatchCallback callback) {
|
||||
@SuppressWarnings("serial")
|
||||
Properties properties = new Properties() {
|
||||
|
||||
@Override
|
||||
public String getProperty(String key) {
|
||||
Object value = get(key);
|
||||
return (value != null ? value.toString() : null);
|
||||
}
|
||||
};
|
||||
properties.putAll(getFlattenedMap(map));
|
||||
|
||||
if (this.documentMatchers.isEmpty()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Merging document (no matchers set): " + map);
|
||||
}
|
||||
callback.process(properties, map);
|
||||
return true;
|
||||
}
|
||||
|
||||
MatchStatus result = MatchStatus.ABSTAIN;
|
||||
for (DocumentMatcher matcher : this.documentMatchers) {
|
||||
MatchStatus match = matcher.matches(properties);
|
||||
result = MatchStatus.getMostSpecific(match, result);
|
||||
if (match == MatchStatus.FOUND) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Matched document with document matcher: " + properties);
|
||||
}
|
||||
callback.process(properties, map);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == MatchStatus.ABSTAIN && this.matchDefault) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Matched document with default matcher: " + map);
|
||||
}
|
||||
callback.process(properties, map);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Unmatched document: " + map);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a flattened version of the given map, recursively following any nested
|
||||
* Map or Collection values. Entries from the resulting map retain the same
|
||||
* order as the source. When called with the Map from a {@link MatchCallback}
|
||||
* the result will contain the same values as the {@link MatchCallback}
|
||||
* Properties.
|
||||
*
|
||||
* @param source the source map
|
||||
* @return a flattened map
|
||||
* @since 4.1.3
|
||||
*/
|
||||
protected final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
|
||||
Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||
buildFlattenedMap(result, source, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean containsText(CharSequence str) {
|
||||
int strLen = str.length();
|
||||
for (int i = 0; i < strLen; i++) {
|
||||
if (!Character.isWhitespace(str.charAt(i))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> source, String path) {
|
||||
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (path != null && !path.isEmpty() && containsText(path)) {
|
||||
if (key.startsWith("[")) {
|
||||
key = path + key;
|
||||
} else {
|
||||
key = path + '.' + key;
|
||||
}
|
||||
}
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof String) {
|
||||
result.put(key, value);
|
||||
} else if (value instanceof Map) {
|
||||
// Need a compound key
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) value;
|
||||
buildFlattenedMap(result, map, key);
|
||||
} else if (value instanceof Collection) {
|
||||
// Need a compound key
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Object> collection = (Collection<Object>) value;
|
||||
int count = 0;
|
||||
for (Object object : collection) {
|
||||
buildFlattenedMap(result, Collections.singletonMap("[" + (count++) + "]", object), key);
|
||||
}
|
||||
} else {
|
||||
result.put(key, (value != null ? value : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface used to process the YAML parsing results.
|
||||
*/
|
||||
public interface MatchCallback {
|
||||
|
||||
/**
|
||||
* Process the given representation of the parsing results.
|
||||
*
|
||||
* @param properties the properties to process (as a flattened representation
|
||||
* with indexed keys in case of a collection or map)
|
||||
* @param map the result map (preserving the original value structure in the
|
||||
* YAML document)
|
||||
*/
|
||||
void process(Properties properties, Map<String, Object> map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface used to test if properties match.
|
||||
*/
|
||||
public interface DocumentMatcher {
|
||||
|
||||
/**
|
||||
* Test if the given properties match.
|
||||
*
|
||||
* @param properties the properties to test
|
||||
* @return the status of the match
|
||||
*/
|
||||
MatchStatus matches(Properties properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Status returned from {@link DocumentMatcher#matches(Properties)}
|
||||
*/
|
||||
public enum MatchStatus {
|
||||
|
||||
/**
|
||||
* A match was found.
|
||||
*/
|
||||
FOUND,
|
||||
|
||||
/**
|
||||
* No match was found.
|
||||
*/
|
||||
NOT_FOUND,
|
||||
|
||||
/**
|
||||
* The matcher should not be considered.
|
||||
*/
|
||||
ABSTAIN;
|
||||
|
||||
/**
|
||||
* Compare two {@link MatchStatus} items, returning the most specific status.
|
||||
*/
|
||||
public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) {
|
||||
return (a.ordinal() < b.ordinal() ? a : b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to use for resolving resources.
|
||||
*/
|
||||
public enum ResolutionMethod {
|
||||
|
||||
/**
|
||||
* Replace values from earlier in the list.
|
||||
*/
|
||||
OVERRIDE,
|
||||
|
||||
/**
|
||||
* Replace values from earlier in the list, ignoring any failures.
|
||||
*/
|
||||
OVERRIDE_AND_IGNORE,
|
||||
|
||||
/**
|
||||
* Take the first resource in the list that exists and use just that.
|
||||
*/
|
||||
FIRST_FOUND
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialized {@link Constructor} that checks for duplicate keys.
|
||||
*/
|
||||
protected static class StrictMapAppenderConstructor extends Constructor {
|
||||
|
||||
// Declared as public for use in subclasses
|
||||
public StrictMapAppenderConstructor(){
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Object, Object> constructMapping(MappingNode node) {
|
||||
try {
|
||||
return super.constructMapping(node);
|
||||
} catch (IllegalStateException ex) {
|
||||
throw new ParserException("while parsing MappingNode",
|
||||
node.getStartMark(),
|
||||
ex.getMessage(),
|
||||
node.getEndMark());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Object, Object> createDefaultMap() {
|
||||
final Map<Object, Object> delegate = super.createDefaultMap();
|
||||
return new AbstractMap<Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Object put(Object key, Object value) {
|
||||
if (delegate.containsKey(key)) {
|
||||
throw new IllegalStateException("Duplicate key: " + key);
|
||||
}
|
||||
return delegate.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<Object, Object>> entrySet() {
|
||||
return delegate.entrySet();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.alibaba.otter.canal.client.adapter.config.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.nodes.Tag;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
import org.yaml.snakeyaml.resolver.Resolver;
|
||||
|
||||
/**
|
||||
* Strategy to load '.yml' (or '.yaml') files into a {@link PropertySource}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class YamlPropertySourceLoader implements PropertySourceLoader {
|
||||
|
||||
@Override
|
||||
public String[] getFileExtensions() {
|
||||
return new String[] { "yml", "yaml" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertySource<?> load(String name, Resource resource, String profile) throws IOException {
|
||||
if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
|
||||
Processor processor = new Processor(resource, profile);
|
||||
Map<String, Object> source = processor.process();
|
||||
if (!source.isEmpty()) {
|
||||
return new MapPropertySource(name, source);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link YamlProcessor} to create a {@link Map} containing the property values.
|
||||
* Similar to {@link YamlPropertiesFactoryBean} but retains the order of
|
||||
* entries.
|
||||
*/
|
||||
private static class Processor extends YamlProcessor {
|
||||
|
||||
Processor(Resource resource, String profile){
|
||||
if (profile == null) {
|
||||
setMatchDefault(true);
|
||||
setDocumentMatchers(new SpringProfileDocumentMatcher());
|
||||
} else {
|
||||
setMatchDefault(false);
|
||||
setDocumentMatchers(new SpringProfileDocumentMatcher(profile));
|
||||
}
|
||||
setResources(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Yaml createYaml() {
|
||||
return new Yaml(new StrictMapAppenderConstructor(), new Representer(), new DumperOptions(), new Resolver() {
|
||||
|
||||
@Override
|
||||
public void addImplicitResolver(Tag tag, Pattern regexp, String first) {
|
||||
if (tag == Tag.TIMESTAMP) {
|
||||
return;
|
||||
}
|
||||
super.addImplicitResolver(tag, regexp, first);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Map<String, Object> process() {
|
||||
final Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||
process(new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public void process(Properties properties, Map<String, Object> map) {
|
||||
result.putAll(getFlattenedMap(map));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 适配器配置集合, 用于配置加载, 线程不安全
|
||||
*
|
||||
* @author rewerma @ 2018-10-20
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class AdapterConfigs {
|
||||
|
||||
/**
|
||||
* 类型下对应所有配置名, 如:
|
||||
* hbase
|
||||
* ┗━ mytest_person.yml
|
||||
* ┗━ mytest_role.yml
|
||||
* ┗━ mytest_department.yml
|
||||
*/
|
||||
private static Map<String, Set<String>> configs = new HashMap<>();
|
||||
|
||||
public static void put(String key, String value) {
|
||||
Set<String> values = configs.get(key);
|
||||
if (values == null) {
|
||||
values = new LinkedHashSet<>();
|
||||
}
|
||||
values.add(value);
|
||||
configs.put(key, values);
|
||||
}
|
||||
|
||||
public static Set<String> get(String key) {
|
||||
return configs.get(key);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
configs.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 配置信息类
|
||||
@@ -11,17 +12,30 @@ import java.util.List;
|
||||
*/
|
||||
public class CanalClientConfig {
|
||||
|
||||
private String canalServerHost; // 单机模式下canal server的 ip:port
|
||||
// 单机模式下canal server的ip:port
|
||||
private String canalServerHost;
|
||||
// 集群模式下的zk地址,如果配置了单机地址则以单机为准!!
|
||||
private String zookeeperHosts;
|
||||
// kafka or rocket mq 地址
|
||||
private String mqServers;
|
||||
// 是否已flatMessage模式传输,只适用于mq模式
|
||||
private Boolean flatMessage = true;
|
||||
// 批大小
|
||||
private Integer batchSize;
|
||||
// 同步分批提交大小
|
||||
private Integer syncBatchSize = 1000;
|
||||
// 重试次数
|
||||
private Integer retries;
|
||||
// 消费超时时间
|
||||
private Long timeout;
|
||||
// 模式 tcp kafka rocketMQ
|
||||
private String mode = "tcp";
|
||||
// aliyun ak/sk
|
||||
private String accessKey;
|
||||
private String secretKey;
|
||||
|
||||
private String zookeeperHosts; // 集群模式下的zk地址, 如果配置了单机地址则以单机为准!!
|
||||
|
||||
private String bootstrapServers; // kafka or rocket mq 地址
|
||||
|
||||
private Boolean flatMessage = true; // 是否已flatMessage模式传输, 只适用于mq模式
|
||||
|
||||
private List<MQTopic> mqTopics; // mq topic 列表
|
||||
|
||||
private List<CanalInstance> canalInstances; // tcp 模式下 canal 实例列表, 与mq模式不能共存!!
|
||||
// canal adapters 配置
|
||||
private List<CanalAdapter> canalAdapters;
|
||||
|
||||
public String getCanalServerHost() {
|
||||
return canalServerHost;
|
||||
@@ -39,20 +53,12 @@ public class CanalClientConfig {
|
||||
this.zookeeperHosts = zookeeperHosts;
|
||||
}
|
||||
|
||||
public String getBootstrapServers() {
|
||||
return bootstrapServers;
|
||||
public String getMqServers() {
|
||||
return mqServers;
|
||||
}
|
||||
|
||||
public void setBootstrapServers(String bootstrapServers) {
|
||||
this.bootstrapServers = bootstrapServers;
|
||||
}
|
||||
|
||||
public List<MQTopic> getMqTopics() {
|
||||
return mqTopics;
|
||||
}
|
||||
|
||||
public void setMqTopics(List<MQTopic> mqTopics) {
|
||||
this.mqTopics = mqTopics;
|
||||
public void setMqServers(String mqServers) {
|
||||
this.mqServers = mqServers;
|
||||
}
|
||||
|
||||
public Boolean getFlatMessage() {
|
||||
@@ -63,17 +69,73 @@ public class CanalClientConfig {
|
||||
this.flatMessage = flatMessage;
|
||||
}
|
||||
|
||||
public List<CanalInstance> getCanalInstances() {
|
||||
return canalInstances;
|
||||
public Integer getBatchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
|
||||
public void setCanalInstances(List<CanalInstance> canalInstances) {
|
||||
this.canalInstances = canalInstances;
|
||||
public void setBatchSize(Integer batchSize) {
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
public static class CanalInstance {
|
||||
public Integer getRetries() {
|
||||
return retries;
|
||||
}
|
||||
|
||||
private String instance; // 实例名
|
||||
public Integer getSyncBatchSize() {
|
||||
return syncBatchSize;
|
||||
}
|
||||
|
||||
public void setSyncBatchSize(Integer syncBatchSize) {
|
||||
this.syncBatchSize = syncBatchSize;
|
||||
}
|
||||
|
||||
public void setRetries(Integer retries) {
|
||||
this.retries = retries;
|
||||
}
|
||||
|
||||
public Long getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(Long timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public void setAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
}
|
||||
|
||||
public String getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(String secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public List<CanalAdapter> getCanalAdapters() {
|
||||
return canalAdapters;
|
||||
}
|
||||
|
||||
public void setCanalAdapters(List<CanalAdapter> canalAdapters) {
|
||||
this.canalAdapters = canalAdapters;
|
||||
}
|
||||
|
||||
public static class CanalAdapter {
|
||||
|
||||
private String instance; // 实例名
|
||||
|
||||
private List<Group> groups; // 适配器分组列表
|
||||
|
||||
@@ -94,60 +156,14 @@ public class CanalClientConfig {
|
||||
public void setGroups(List<Group> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Group {
|
||||
|
||||
private List<OuterAdapterConfig> outAdapters; // 适配器列表
|
||||
|
||||
public List<OuterAdapterConfig> getOutAdapters() {
|
||||
return outAdapters;
|
||||
}
|
||||
|
||||
public void setOutAdapters(List<OuterAdapterConfig> outAdapters) {
|
||||
this.outAdapters = outAdapters;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MQTopic {
|
||||
|
||||
private String mqMode; // mq模式 kafka or rocketMQ
|
||||
|
||||
private String topic; // topic名
|
||||
|
||||
private List<MQGroup> groups = new ArrayList<>(); // 分组列表
|
||||
|
||||
public String getMqMode() {
|
||||
return mqMode;
|
||||
}
|
||||
|
||||
public void setMqMode(String mqMode) {
|
||||
this.mqMode = mqMode;
|
||||
}
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public void setTopic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public List<MQGroup> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(List<MQGroup> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MQGroup {
|
||||
|
||||
private String groupId; // group id
|
||||
|
||||
private List<OuterAdapterConfig> outAdapters; // 适配器配置列表
|
||||
// group id
|
||||
private String groupId = "default";
|
||||
private List<OuterAdapterConfig> outerAdapters; // 适配器列表
|
||||
private Map<String, OuterAdapterConfig> outerAdaptersMap = new LinkedHashMap<>();
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
@@ -157,14 +173,20 @@ public class CanalClientConfig {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public List<OuterAdapterConfig> getOutAdapters() {
|
||||
return outAdapters;
|
||||
public List<OuterAdapterConfig> getOuterAdapters() {
|
||||
return outerAdapters;
|
||||
}
|
||||
|
||||
public void setOutAdapters(List<OuterAdapterConfig> outAdapters) {
|
||||
this.outAdapters = outAdapters;
|
||||
public void setOuterAdapters(List<OuterAdapterConfig> outerAdapters) {
|
||||
this.outerAdapters = outerAdapters;
|
||||
}
|
||||
|
||||
public Map<String, OuterAdapterConfig> getOuterAdaptersMap() {
|
||||
return outerAdaptersMap;
|
||||
}
|
||||
|
||||
public void setOuterAdaptersMap(Map<String, OuterAdapterConfig> outerAdaptersMap) {
|
||||
this.outerAdaptersMap = outerAdaptersMap;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
public class Constant {
|
||||
|
||||
public static final String CONF_DIR = "conf";
|
||||
}
|
||||
@@ -15,8 +15,11 @@ public class Dml implements Serializable {
|
||||
private static final long serialVersionUID = 2611556444074013268L;
|
||||
|
||||
private String destination; // 对应canal的实例或者MQ的topic
|
||||
private String groupId; // 对应mq的group id
|
||||
private String database; // 数据库或schema
|
||||
private String table; // 表名
|
||||
private List<String> pkNames;
|
||||
private Boolean isDdl;
|
||||
private String type; // 类型: INSERT UPDATE DELETE
|
||||
// binlog executeTime
|
||||
private Long es; // 执行耗时
|
||||
@@ -34,6 +37,14 @@ public class Dml implements Serializable {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getDatabase() {
|
||||
return database;
|
||||
}
|
||||
@@ -50,6 +61,22 @@ public class Dml implements Serializable {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public List<String> getPkNames() {
|
||||
return pkNames;
|
||||
}
|
||||
|
||||
public void setPkNames(List<String> pkNames) {
|
||||
this.pkNames = pkNames;
|
||||
}
|
||||
|
||||
public Boolean getIsDdl() {
|
||||
return isDdl;
|
||||
}
|
||||
|
||||
public void setIsDdl(Boolean isDdl) {
|
||||
this.isDdl = isDdl;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -37,6 +44,8 @@ public class ExtensionLoader<T> {
|
||||
|
||||
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ConcurrentMap<String, Object> EXTENSION_KEY_INSTANCE = new ConcurrentHashMap<>();
|
||||
|
||||
private final Class<?> type;
|
||||
|
||||
private final String classLoaderPolicy;
|
||||
@@ -83,47 +92,6 @@ public class ExtensionLoader<T> {
|
||||
this.classLoaderPolicy = classLoaderPolicy;
|
||||
}
|
||||
|
||||
public String getExtensionName(T extensionInstance) {
|
||||
return getExtensionName(extensionInstance.getClass());
|
||||
}
|
||||
|
||||
public String getExtensionName(Class<?> extensionClass) {
|
||||
return cachedNames.get(extensionClass);
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, IllegalStateException> getExceptions() {
|
||||
return exceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回扩展点实例,如果没有指定的扩展点或是还没加载(即实例化)则返回<code>null</code>注意:此方法不会触发扩展点的加载
|
||||
* <p/>
|
||||
* 一般应该调用{@link #getExtension(String)}方法获得扩展,这个方法会触发扩展点加载
|
||||
*
|
||||
* @see #getExtension(String)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public T getLoadedExtension(String name) {
|
||||
if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null");
|
||||
Holder<Object> holder = cachedInstances.get(name);
|
||||
if (holder == null) {
|
||||
cachedInstances.putIfAbsent(name, new Holder<>());
|
||||
holder = cachedInstances.get(name);
|
||||
}
|
||||
return (T) holder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回已经加载的扩展点的名字
|
||||
* <p/>
|
||||
* 一般应该调用{@link #getSupportedExtensions()}方法获得扩展,这个方法会返回所有的扩展点
|
||||
*
|
||||
* @see #getSupportedExtensions()
|
||||
*/
|
||||
public Set<String> getLoadedExtensions() {
|
||||
return Collections.unmodifiableSet(new TreeSet<>(cachedInstances.keySet()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定名字的扩展
|
||||
*
|
||||
@@ -154,6 +122,31 @@ public class ExtensionLoader<T> {
|
||||
return (T) instance;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T getExtension(String name, String key) {
|
||||
if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null");
|
||||
if ("true".equals(name)) {
|
||||
return getDefaultExtension();
|
||||
}
|
||||
String extKey = name + "-" + StringUtils.trimToEmpty(key);
|
||||
Holder<Object> holder = cachedInstances.get(extKey);
|
||||
if (holder == null) {
|
||||
cachedInstances.putIfAbsent(extKey, new Holder<>());
|
||||
holder = cachedInstances.get(extKey);
|
||||
}
|
||||
Object instance = holder.get();
|
||||
if (instance == null) {
|
||||
synchronized (holder) {
|
||||
instance = holder.get();
|
||||
if (instance == null) {
|
||||
instance = createExtension(name, key);
|
||||
holder.set(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (T) instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回缺省的扩展,如果没有设置则返回<code>null</code>
|
||||
*/
|
||||
@@ -165,87 +158,6 @@ public class ExtensionLoader<T> {
|
||||
return getExtension(cachedDefaultName);
|
||||
}
|
||||
|
||||
public boolean hasExtension(String name) {
|
||||
if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null");
|
||||
try {
|
||||
return getExtensionClass(name) != null;
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getSupportedExtensions() {
|
||||
Map<String, Class<?>> clazzes = getExtensionClasses();
|
||||
return Collections.unmodifiableSet(new TreeSet<String>(clazzes.keySet()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回缺省的扩展点名,如果没有设置缺省则返回<code>null</code>
|
||||
*/
|
||||
public String getDefaultExtensionName() {
|
||||
getExtensionClasses();
|
||||
return cachedDefaultName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编程方式添加新扩展点
|
||||
*
|
||||
* @param name 扩展点名
|
||||
* @param clazz 扩展点类
|
||||
* @throws IllegalStateException 要添加扩展点名已经存在
|
||||
*/
|
||||
public void addExtension(String name, Class<?> clazz) {
|
||||
getExtensionClasses(); // load classes
|
||||
|
||||
if (!type.isAssignableFrom(clazz)) {
|
||||
throw new IllegalStateException("Input type " + clazz + "not implement Extension " + type);
|
||||
}
|
||||
if (clazz.isInterface()) {
|
||||
throw new IllegalStateException("Input type " + clazz + "can not be interface!");
|
||||
}
|
||||
|
||||
if (name == null || "".equals(name)) {
|
||||
throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
|
||||
}
|
||||
if (cachedClasses.get().containsKey(name)) {
|
||||
throw new IllegalStateException("Extension name " + name + " already existed(Extension " + type + ")!");
|
||||
}
|
||||
|
||||
cachedNames.put(clazz, name);
|
||||
cachedClasses.get().put(name, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编程方式添加替换已有扩展点
|
||||
*
|
||||
* @param name 扩展点名
|
||||
* @param clazz 扩展点类
|
||||
* @throws IllegalStateException 要添加扩展点名已经存在
|
||||
* @deprecated 不推荐应用使用,一般只在测试时可以使用
|
||||
*/
|
||||
@Deprecated
|
||||
public void replaceExtension(String name, Class<?> clazz) {
|
||||
getExtensionClasses(); // load classes
|
||||
|
||||
if (!type.isAssignableFrom(clazz)) {
|
||||
throw new IllegalStateException("Input type " + clazz + "not implement Extension " + type);
|
||||
}
|
||||
if (clazz.isInterface()) {
|
||||
throw new IllegalStateException("Input type " + clazz + "can not be interface!");
|
||||
}
|
||||
|
||||
if (name == null || "".equals(name)) {
|
||||
throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
|
||||
}
|
||||
if (!cachedClasses.get().containsKey(name)) {
|
||||
throw new IllegalStateException("Extension name " + name + " not existed(Extension " + type + ")!");
|
||||
}
|
||||
|
||||
cachedNames.put(clazz, name);
|
||||
cachedClasses.get().put(name, clazz);
|
||||
cachedInstances.remove(name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private T createExtension(String name) {
|
||||
Class<?> clazz = getExtensionClasses().get(name);
|
||||
@@ -267,13 +179,25 @@ public class ExtensionLoader<T> {
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?> getExtensionClass(String name) {
|
||||
if (type == null) throw new IllegalArgumentException("Extension type == null");
|
||||
if (name == null) throw new IllegalArgumentException("Extension name == null");
|
||||
@SuppressWarnings("unchecked")
|
||||
private T createExtension(String name, String key) {
|
||||
Class<?> clazz = getExtensionClasses().get(name);
|
||||
if (clazz == null)
|
||||
throw new IllegalStateException("No such extension \"" + name + "\" for " + type.getName() + "!");
|
||||
return clazz;
|
||||
if (clazz == null) {
|
||||
throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type
|
||||
+ ") could not be instantiated: class could not be found");
|
||||
}
|
||||
try {
|
||||
T instance = (T) EXTENSION_KEY_INSTANCE.get(name + "-" + key);
|
||||
if (instance == null) {
|
||||
EXTENSION_KEY_INSTANCE.putIfAbsent(name + "-" + key, clazz.newInstance());
|
||||
instance = (T) EXTENSION_KEY_INSTANCE.get(name + "-" + key);
|
||||
}
|
||||
return instance;
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type
|
||||
+ ") could not be instantiated: " + t.getMessage(),
|
||||
t);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Class<?>> getExtensionClasses() {
|
||||
@@ -287,15 +211,19 @@ public class ExtensionLoader<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
private String getJarDirectoryPath() {
|
||||
URL url = Thread.currentThread().getContextClassLoader().getResource("");
|
||||
if (url == null) {
|
||||
throw new IllegalStateException("failed to get class loader resource");
|
||||
String dirtyPath;
|
||||
if (url != null) {
|
||||
dirtyPath = url.toString();
|
||||
} else {
|
||||
File file = new File("");
|
||||
dirtyPath = file.getAbsolutePath();
|
||||
}
|
||||
String dirtyPath = url.toString();
|
||||
String jarPath = dirtyPath.replaceAll("^.*file:/", ""); // removes
|
||||
// file:/ and
|
||||
// everything
|
||||
@@ -310,7 +238,11 @@ public class ExtensionLoader<T> {
|
||||
// button.
|
||||
jarPath = jarPath.replaceAll("/classes/.*", "/classes/");
|
||||
}
|
||||
return Paths.get(jarPath).getParent().toString(); // Paths - from java 8
|
||||
Path path = Paths.get(jarPath).getParent(); // Paths - from java 8
|
||||
if (path != null) {
|
||||
return path.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Class<?>> loadExtensionClasses() {
|
||||
@@ -329,25 +261,20 @@ public class ExtensionLoader<T> {
|
||||
|
||||
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
|
||||
|
||||
// 1. lib folder,customized extension classLoader (jar_dir/lib)
|
||||
String dir = File.separator + this.getJarDirectoryPath() + File.separator + "lib";
|
||||
logger.info("extension classpath dir: " + dir);
|
||||
// 1. plugin folder,customized extension classLoader (jar_dir/plugin)
|
||||
String dir = File.separator + this.getJarDirectoryPath() + File.separator + "plugin";
|
||||
|
||||
File externalLibDir = new File(dir);
|
||||
if (!externalLibDir.exists()) {
|
||||
externalLibDir = new File(
|
||||
File.separator + this.getJarDirectoryPath() + File.separator + "canal_client" + File.separator + "lib");
|
||||
externalLibDir = new File(File.separator + this.getJarDirectoryPath() + File.separator + "canal-adapter"
|
||||
+ File.separator + "plugin");
|
||||
}
|
||||
logger.info("extension classpath dir: " + externalLibDir.getAbsolutePath());
|
||||
if (externalLibDir.exists()) {
|
||||
File[] files = externalLibDir.listFiles(new FilenameFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.endsWith(".jar");
|
||||
}
|
||||
});
|
||||
File[] files = externalLibDir.listFiles((dir1, name) -> name.endsWith(".jar"));
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
URL url = null;
|
||||
URL url;
|
||||
try {
|
||||
url = f.toURI().toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
@@ -358,49 +285,7 @@ public class ExtensionLoader<T> {
|
||||
URLClassLoader localClassLoader;
|
||||
if (classLoaderPolicy == null || "".equals(classLoaderPolicy)
|
||||
|| DEFAULT_CLASSLOADER_POLICY.equalsIgnoreCase(classLoaderPolicy)) {
|
||||
localClassLoader = new URLClassLoader(new URL[] { url }, parent) {
|
||||
|
||||
@Override
|
||||
public Class<?> loadClass(String name) throws ClassNotFoundException {
|
||||
Class<?> c = findLoadedClass(name);
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
if (name.startsWith("java.") || name.startsWith("org.slf4j.")
|
||||
|| name.startsWith("org.apache.logging")
|
||||
|| name.startsWith("org.apache.commons.logging.")) {
|
||||
// || name.startsWith("org.apache.hadoop."))
|
||||
// {
|
||||
c = super.loadClass(name);
|
||||
}
|
||||
if (c != null) return c;
|
||||
|
||||
try {
|
||||
// 先加载jar内的class,可避免jar冲突
|
||||
c = findClass(name);
|
||||
} catch (ClassNotFoundException e) {
|
||||
c = null;
|
||||
}
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
return super.loadClass(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
@SuppressWarnings("unchecked")
|
||||
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
|
||||
|
||||
tmp[0] = findResources(name); // local class
|
||||
// path first
|
||||
// tmp[1] = super.getResources(name);
|
||||
|
||||
return new CompoundEnumeration<>(tmp);
|
||||
}
|
||||
};
|
||||
localClassLoader = new URLClassExtensionLoader(new URL[] { url });
|
||||
} else {
|
||||
localClassLoader = new URLClassLoader(new URL[] { url }, parent);
|
||||
}
|
||||
@@ -410,48 +295,15 @@ public class ExtensionLoader<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 只加载外部spi, 不加载classpath
|
||||
// 2. load inner extension class with default classLoader
|
||||
ClassLoader classLoader = findClassLoader();
|
||||
loadFile(extensionClasses, CANAL_DIRECTORY, classLoader);
|
||||
loadFile(extensionClasses, SERVICES_DIRECTORY, classLoader);
|
||||
// ClassLoader classLoader = findClassLoader();
|
||||
// loadFile(extensionClasses, CANAL_DIRECTORY, classLoader);
|
||||
// loadFile(extensionClasses, SERVICES_DIRECTORY, classLoader);
|
||||
|
||||
return extensionClasses;
|
||||
}
|
||||
|
||||
public static class CompoundEnumeration<E> implements Enumeration<E> {
|
||||
|
||||
private Enumeration<E>[] enums;
|
||||
private int index = 0;
|
||||
|
||||
public CompoundEnumeration(Enumeration<E>[] enums){
|
||||
this.enums = enums;
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
while (this.index < this.enums.length) {
|
||||
if (this.enums[this.index] != null && this.enums[this.index].hasMoreElements()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
++this.index;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasMoreElements() {
|
||||
return this.next();
|
||||
}
|
||||
|
||||
public E nextElement() {
|
||||
if (!this.next()) {
|
||||
throw new NoSuchElementException();
|
||||
} else {
|
||||
return this.enums[this.index].nextElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFile(Map<String, Class<?>> extensionClasses, String dir, ClassLoader classLoader) {
|
||||
String fileName = dir + type.getName();
|
||||
try {
|
||||
@@ -549,6 +401,7 @@ public class ExtensionLoader<T> {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static ClassLoader findClassLoader() {
|
||||
return ExtensionLoader.class.getClassLoader();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.sql.*;
|
||||
import java.sql.Date;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Time;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -67,8 +71,14 @@ public class JdbcTypeUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static Object typeConvert(String columnName, String value, int sqlType, String mysqlType) {
|
||||
if (value == null || value.equals("")) {
|
||||
private static boolean isText(String columnType) {
|
||||
return "LONGTEXT".equalsIgnoreCase(columnType) || "MEDIUMTEXT".equalsIgnoreCase(columnType)
|
||||
|| "TEXT".equalsIgnoreCase(columnType) || "TINYTEXT".equalsIgnoreCase(columnType);
|
||||
}
|
||||
|
||||
public static Object typeConvert(String tableName ,String columnName, String value, int sqlType, String mysqlType) {
|
||||
if (value == null
|
||||
|| (value.equals("") && !(isText(mysqlType) || sqlType == Types.CHAR || sqlType == Types.VARCHAR || sqlType == Types.LONGVARCHAR))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -115,23 +125,33 @@ public class JdbcTypeUtil {
|
||||
break;
|
||||
case Types.DATE:
|
||||
if (!value.startsWith("0000-00-00")) {
|
||||
value = value.trim().replace(" ", "T");
|
||||
DateTime dt = new DateTime(value);
|
||||
res = new Date(dt.toDate().getTime());
|
||||
java.util.Date date = Util.parseDate(value);
|
||||
if (date != null) {
|
||||
res = new Date(date.getTime());
|
||||
} else {
|
||||
res = null;
|
||||
}
|
||||
} else {
|
||||
res = null;
|
||||
}
|
||||
break;
|
||||
case Types.TIME:
|
||||
value = "T" + value;
|
||||
DateTime dt = new DateTime(value);
|
||||
res = new Time(dt.toDate().getTime());
|
||||
case Types.TIME: {
|
||||
java.util.Date date = Util.parseDate(value);
|
||||
if (date != null) {
|
||||
res = new Time(date.getTime());
|
||||
} else {
|
||||
res = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Types.TIMESTAMP:
|
||||
if (!value.startsWith("0000-00-00")) {
|
||||
value = value.trim().replace(" ", "T");
|
||||
dt = new DateTime(value);
|
||||
res = new Timestamp(dt.toDate().getTime());
|
||||
java.util.Date date = Util.parseDate(value);
|
||||
if (date != null) {
|
||||
res = new Timestamp(date.getTime());
|
||||
} else {
|
||||
res = null;
|
||||
}
|
||||
} else {
|
||||
res = null;
|
||||
}
|
||||
@@ -139,10 +159,11 @@ public class JdbcTypeUtil {
|
||||
case Types.CLOB:
|
||||
default:
|
||||
res = value;
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
} catch (Exception e) {
|
||||
logger.error("table: {} column: {}, failed convert type {} to {}", columnName, value, sqlType);
|
||||
logger.error("table: {} column: {}, failed convert type {} to {}", tableName, columnName, value, sqlType);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MappingConfigsLoader {
|
||||
|
||||
public static Map<String, String> loadConfigs(String name) {
|
||||
Map<String, String> configContentMap = new HashMap<>();
|
||||
|
||||
// 先取本地文件,再取类路径
|
||||
File configDir = new File(".." + File.separator + Constant.CONF_DIR + File.separator + name);
|
||||
if (!configDir.exists()) {
|
||||
URL url = MappingConfigsLoader.class.getClassLoader().getResource("");
|
||||
if (url != null) {
|
||||
configDir = new File(url.getPath() + name + File.separator);
|
||||
}
|
||||
}
|
||||
|
||||
File[] files = configDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
String fileName = file.getName();
|
||||
if (!fileName.endsWith(".yml")) {
|
||||
continue;
|
||||
}
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
byte[] bytes = new byte[in.available()];
|
||||
in.read(bytes);
|
||||
String configContent = new String(bytes, StandardCharsets.UTF_8);
|
||||
configContentMap.put(fileName, configContent);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Read " + name + "mapping config: " + fileName + " error. ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configContentMap;
|
||||
}
|
||||
|
||||
public static String loadConfig(String name) {
|
||||
// 先取本地文件,再取类路径
|
||||
File filePath = new File(".." + File.separator + Constant.CONF_DIR + File.separator + name);
|
||||
if (!filePath.exists()) {
|
||||
URL url = MappingConfigsLoader.class.getClassLoader().getResource("");
|
||||
if (url != null) {
|
||||
filePath = new File(url.getPath() + name);
|
||||
}
|
||||
}
|
||||
if (filePath.exists()) {
|
||||
String fileName = filePath.getName();
|
||||
if (!fileName.endsWith(".yml")) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream in = new FileInputStream(filePath)) {
|
||||
byte[] bytes = new byte[in.available()];
|
||||
in.read(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Read mapping config: " + filePath.getAbsolutePath() + " error. ", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import com.alibaba.otter.canal.protocol.CanalEntry;
|
||||
import com.alibaba.otter.canal.protocol.FlatMessage;
|
||||
@@ -19,12 +14,12 @@ import com.alibaba.otter.canal.protocol.Message;
|
||||
*/
|
||||
public class MessageUtil {
|
||||
|
||||
public static void parse4Dml(String destination, Message message, Consumer<Dml> consumer) {
|
||||
public static List<Dml> parse4Dml(String destination, String groupId, Message message) {
|
||||
if (message == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
List<CanalEntry.Entry> entries = message.getEntries();
|
||||
|
||||
List<Dml> dmls = new ArrayList<Dml>(entries.size());
|
||||
for (CanalEntry.Entry entry : entries) {
|
||||
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN
|
||||
|| entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
|
||||
@@ -42,18 +37,24 @@ public class MessageUtil {
|
||||
CanalEntry.EventType eventType = rowChange.getEventType();
|
||||
|
||||
final Dml dml = new Dml();
|
||||
dml.setIsDdl(rowChange.getIsDdl());
|
||||
dml.setDestination(destination);
|
||||
dml.setGroupId(groupId);
|
||||
dml.setDatabase(entry.getHeader().getSchemaName());
|
||||
dml.setTable(entry.getHeader().getTableName());
|
||||
dml.setType(eventType.toString());
|
||||
dml.setEs(entry.getHeader().getExecuteTime());
|
||||
dml.setIsDdl(rowChange.getIsDdl());
|
||||
dml.setTs(System.currentTimeMillis());
|
||||
dml.setSql(rowChange.getSql());
|
||||
dmls.add(dml);
|
||||
List<Map<String, Object>> data = new ArrayList<>();
|
||||
List<Map<String, Object>> old = new ArrayList<>();
|
||||
|
||||
if (!rowChange.getIsDdl()) {
|
||||
Set<String> updateSet = new HashSet<>();
|
||||
dml.setPkNames(new ArrayList<>());
|
||||
int i = 0;
|
||||
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
|
||||
if (eventType != CanalEntry.EventType.INSERT && eventType != CanalEntry.EventType.UPDATE
|
||||
&& eventType != CanalEntry.EventType.DELETE) {
|
||||
@@ -70,8 +71,13 @@ public class MessageUtil {
|
||||
}
|
||||
|
||||
for (CanalEntry.Column column : columns) {
|
||||
if (i == 0) {
|
||||
if (column.getIsKey()) {
|
||||
dml.getPkNames().add(column.getName());
|
||||
}
|
||||
}
|
||||
row.put(column.getName(),
|
||||
JdbcTypeUtil.typeConvert(column.getName(),
|
||||
JdbcTypeUtil.typeConvert(dml.getTable(),column.getName(),
|
||||
column.getValue(),
|
||||
column.getSqlType(),
|
||||
column.getMysqlType()));
|
||||
@@ -89,7 +95,7 @@ public class MessageUtil {
|
||||
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
|
||||
if (updateSet.contains(column.getName())) {
|
||||
rowOld.put(column.getName(),
|
||||
JdbcTypeUtil.typeConvert(column.getName(),
|
||||
JdbcTypeUtil.typeConvert(dml.getTable(),column.getName(),
|
||||
column.getValue(),
|
||||
column.getSqlType(),
|
||||
column.getMysqlType()));
|
||||
@@ -100,6 +106,8 @@ public class MessageUtil {
|
||||
old.add(rowOld);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
if (!data.isEmpty()) {
|
||||
dml.setData(data);
|
||||
@@ -108,38 +116,53 @@ public class MessageUtil {
|
||||
dml.setOld(old);
|
||||
}
|
||||
}
|
||||
|
||||
consumer.accept(dml);
|
||||
}
|
||||
|
||||
return dmls;
|
||||
}
|
||||
|
||||
public static Dml flatMessage2Dml(String destination, FlatMessage flatMessage) {
|
||||
public static List<Dml> flatMessage2Dml(String destination, String groupId, List<FlatMessage> flatMessages) {
|
||||
List<Dml> dmls = new ArrayList<Dml>(flatMessages.size());
|
||||
for (FlatMessage flatMessage : flatMessages) {
|
||||
Dml dml = flatMessage2Dml(destination, groupId, flatMessage);
|
||||
if (dml != null) {
|
||||
dmls.add(dml);
|
||||
}
|
||||
}
|
||||
|
||||
return dmls;
|
||||
}
|
||||
|
||||
public static Dml flatMessage2Dml(String destination, String groupId, FlatMessage flatMessage) {
|
||||
if (flatMessage == null) {
|
||||
return null;
|
||||
}
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination(destination);
|
||||
dml.setGroupId(groupId);
|
||||
dml.setDatabase(flatMessage.getDatabase());
|
||||
dml.setTable(flatMessage.getTable());
|
||||
dml.setPkNames(flatMessage.getPkNames());
|
||||
dml.setIsDdl(flatMessage.getIsDdl());
|
||||
dml.setType(flatMessage.getType());
|
||||
dml.setTs(flatMessage.getTs());
|
||||
dml.setEs(flatMessage.getEs());
|
||||
dml.setSql(flatMessage.getSql());
|
||||
if (flatMessage.getSqlType() == null || flatMessage.getMysqlType() == null) {
|
||||
throw new RuntimeException("SqlType or mysqlType is null");
|
||||
}
|
||||
// if (flatMessage.getSqlType() == null || flatMessage.getMysqlType() == null) {
|
||||
// throw new RuntimeException("SqlType or mysqlType is null");
|
||||
// }
|
||||
List<Map<String, String>> data = flatMessage.getData();
|
||||
if (data != null) {
|
||||
dml.setData(changeRows(data, flatMessage.getSqlType(), flatMessage.getMysqlType()));
|
||||
dml.setData(changeRows(dml.getTable(), data, flatMessage.getSqlType(), flatMessage.getMysqlType()));
|
||||
}
|
||||
List<Map<String, String>> old = flatMessage.getOld();
|
||||
if (old != null) {
|
||||
dml.setOld(changeRows(old, flatMessage.getSqlType(), flatMessage.getMysqlType()));
|
||||
dml.setOld(changeRows(dml.getTable(), old, flatMessage.getSqlType(), flatMessage.getMysqlType()));
|
||||
}
|
||||
return dml;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> changeRows(List<Map<String, String>> rows, Map<String, Integer> sqlTypes,
|
||||
private static List<Map<String, Object>> changeRows(String table, List<Map<String, String>> rows, Map<String, Integer> sqlTypes,
|
||||
Map<String, String> mysqlTypes) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, String> row : rows) {
|
||||
@@ -158,16 +181,11 @@ public class MessageUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object finalValue = JdbcTypeUtil.typeConvert(columnName, columnValue, sqlType, mysqlType);
|
||||
Object finalValue = JdbcTypeUtil.typeConvert(table, columnName, columnValue, sqlType, mysqlType);
|
||||
resultRow.put(columnName, finalValue);
|
||||
}
|
||||
result.add(resultRow);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public interface Consumer<T> {
|
||||
|
||||
void accept(T t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public class OuterAdapterConfig {
|
||||
|
||||
private String name; // 适配器名称, 如: logger, hbase, es
|
||||
|
||||
private String key; // 适配器唯一键
|
||||
|
||||
private String hosts; // 适配器内部的地址, 比如对应es该参数可以填写es的server地址
|
||||
|
||||
private String zkHosts; // 适配器内部的ZK地址, 比如对应HBase该参数可以填写HBase对应的ZK地址
|
||||
@@ -26,6 +28,14 @@ public class OuterAdapterConfig {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getHosts() {
|
||||
return hosts;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import java.util.Date;
|
||||
public class Result implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -3276409502352405716L;
|
||||
public Integer code = 20000;
|
||||
public Object data;
|
||||
public String message;
|
||||
public Date sysTime;
|
||||
private Integer code = 20000;
|
||||
private Object data;
|
||||
private String message;
|
||||
private Date sysTime;
|
||||
|
||||
public static Result createSuccess(String message) {
|
||||
Result result = new Result();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public class URLClassExtensionLoader extends URLClassLoader {
|
||||
public URLClassExtensionLoader(URL[] urls) {
|
||||
super(urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> loadClass(String name) throws ClassNotFoundException {
|
||||
Class<?> c = findLoadedClass(name);
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
if (name.startsWith("java.") || name.startsWith("org.slf4j.")
|
||||
|| name.startsWith("org.apache.logging")
|
||||
|| name.startsWith("org.apache.commons.logging.")) {
|
||||
// || name.startsWith("org.apache.hadoop."))
|
||||
// {
|
||||
c = super.loadClass(name);
|
||||
}
|
||||
if (c != null) return c;
|
||||
|
||||
try {
|
||||
// 先加载jar内的class,可避免jar冲突
|
||||
c = findClass(name);
|
||||
} catch (ClassNotFoundException e) {
|
||||
c = null;
|
||||
}
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
return super.loadClass(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
@SuppressWarnings("unchecked")
|
||||
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
|
||||
|
||||
tmp[0] = findResources(name); // local class
|
||||
// path first
|
||||
// tmp[1] = super.getResources(name);
|
||||
|
||||
return new CompoundEnumeration<>(tmp);
|
||||
}
|
||||
|
||||
private static class CompoundEnumeration<E> implements Enumeration<E> {
|
||||
|
||||
private Enumeration<E>[] enums;
|
||||
private int index = 0;
|
||||
|
||||
public CompoundEnumeration(Enumeration<E>[] enums){
|
||||
this.enums = enums;
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
while (this.index < this.enums.length) {
|
||||
if (this.enums[this.index] != null && this.enums[this.index].hasMoreElements()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
++this.index;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasMoreElements() {
|
||||
return this.next();
|
||||
}
|
||||
|
||||
public E nextElement() {
|
||||
if (!this.next()) {
|
||||
throw new NoSuchElementException();
|
||||
} else {
|
||||
return this.enums[this.index].nextElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
|
||||
public class Util {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Util.class);
|
||||
|
||||
/**
|
||||
* 通过DS执行sql
|
||||
*/
|
||||
public static Object sqlRS(DataSource ds, String sql, Function<ResultSet, Object> fun) {
|
||||
try (Connection conn = ds.getConnection();
|
||||
Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);) {
|
||||
stmt.setFetchSize(Integer.MIN_VALUE);
|
||||
try (ResultSet rs = stmt.executeQuery(sql);) {
|
||||
return fun.apply(rs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("sqlRs has error, sql: {} ", sql);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sql执行获取resultSet
|
||||
*
|
||||
* @param conn sql connection
|
||||
* @param sql sql
|
||||
* @param consumer 回调方法
|
||||
*/
|
||||
public static void sqlRS(Connection conn, String sql, Consumer<ResultSet> consumer) {
|
||||
try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) {
|
||||
consumer.accept(rs);
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static File getConfDirPath() {
|
||||
return getConfDirPath("");
|
||||
}
|
||||
|
||||
public static File getConfDirPath(String subConf) {
|
||||
URL url = Util.class.getClassLoader().getResource("");
|
||||
String path;
|
||||
if (url != null) {
|
||||
path = url.getPath();
|
||||
} else {
|
||||
path = new File("").getAbsolutePath();
|
||||
}
|
||||
File file = null;
|
||||
if (path != null) {
|
||||
file = new File(
|
||||
path + ".." + File.separator + Constant.CONF_DIR + File.separator + StringUtils.trimToEmpty(subConf));
|
||||
if (!file.exists()) {
|
||||
file = new File(path + StringUtils.trimToEmpty(subConf));
|
||||
}
|
||||
}
|
||||
if (file == null || !file.exists()) {
|
||||
throw new RuntimeException("Config dir not found.");
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public static String cleanColumn(String column) {
|
||||
if (column == null) {
|
||||
return null;
|
||||
}
|
||||
if (column.contains("`")) {
|
||||
column = column.replaceAll("`", "");
|
||||
}
|
||||
|
||||
if (column.contains("'")) {
|
||||
column = column.replaceAll("'", "");
|
||||
}
|
||||
|
||||
if (column.contains("\"")) {
|
||||
column = column.replaceAll("\"", "");
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor newFixedThreadPool(int nThreads, long keepAliveTime) {
|
||||
return new ThreadPoolExecutor(nThreads,
|
||||
nThreads,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor newSingleThreadExecutor(long keepAliveTime) {
|
||||
return new ThreadPoolExecutor(1,
|
||||
1,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public final static String timeZone; // 当前时区
|
||||
private static DateTimeZone dateTimeZone;
|
||||
|
||||
static {
|
||||
TimeZone localTimeZone = TimeZone.getDefault();
|
||||
int rawOffset = localTimeZone.getRawOffset();
|
||||
String symbol = "+";
|
||||
if (rawOffset < 0) {
|
||||
symbol = "-";
|
||||
}
|
||||
rawOffset = Math.abs(rawOffset);
|
||||
int offsetHour = rawOffset / 3600000;
|
||||
int offsetMinute = rawOffset % 3600000 / 60000;
|
||||
String hour = String.format("%1$02d", offsetHour);
|
||||
String minute = String.format("%1$02d", offsetMinute);
|
||||
timeZone = symbol + hour + ":" + minute;
|
||||
dateTimeZone = DateTimeZone.forID(timeZone);
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("GMT" + timeZone));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用日期时间字符解析
|
||||
*
|
||||
* @param datetimeStr 日期时间字符串
|
||||
* @return Date
|
||||
*/
|
||||
public static Date parseDate(String datetimeStr) {
|
||||
if (StringUtils.isEmpty(datetimeStr)) {
|
||||
return null;
|
||||
}
|
||||
datetimeStr = datetimeStr.trim();
|
||||
if (datetimeStr.contains("-")) {
|
||||
if (datetimeStr.contains(":")) {
|
||||
datetimeStr = datetimeStr.replace(" ", "T");
|
||||
}
|
||||
} else if (datetimeStr.contains(":")) {
|
||||
datetimeStr = "T" + datetimeStr;
|
||||
}
|
||||
|
||||
DateTime dateTime = new DateTime(datetimeStr, dateTimeZone);
|
||||
|
||||
return dateTime.toDate();
|
||||
}
|
||||
|
||||
private static LoadingCache<String, DateTimeFormatter> dateFormatterCache = CacheBuilder.newBuilder()
|
||||
.build(new CacheLoader<String, DateTimeFormatter>() {
|
||||
|
||||
@Override
|
||||
public DateTimeFormatter load(String key) {
|
||||
return DateTimeFormatter.ofPattern(key);
|
||||
}
|
||||
});
|
||||
|
||||
public static Date parseDate2(String datetimeStr) {
|
||||
if (StringUtils.isEmpty(datetimeStr)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
datetimeStr = datetimeStr.trim();
|
||||
int len = datetimeStr.length();
|
||||
if (datetimeStr.contains("-") && datetimeStr.contains(":") && datetimeStr.contains(".")) {
|
||||
// 包含日期+时间+毫秒
|
||||
// 取毫秒位数
|
||||
int msLen = len - datetimeStr.indexOf(".") - 1;
|
||||
StringBuilder ms = new StringBuilder();
|
||||
for (int i = 0; i < msLen; i++) {
|
||||
ms.append("S");
|
||||
}
|
||||
String formatter = "yyyy-MM-dd HH:mm:ss." + ms;
|
||||
|
||||
DateTimeFormatter dateTimeFormatter = dateFormatterCache.get(formatter);
|
||||
LocalDateTime dateTime = LocalDateTime.parse(datetimeStr, dateTimeFormatter);
|
||||
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} else if (datetimeStr.contains("-") && datetimeStr.contains(":")) {
|
||||
// 包含日期+时间
|
||||
// 判断包含时间位数
|
||||
int i = datetimeStr.indexOf(":");
|
||||
i = datetimeStr.indexOf(":", i + 1);
|
||||
String formatter;
|
||||
if (i > -1) {
|
||||
formatter = "yyyy-MM-dd HH:mm:ss";
|
||||
} else {
|
||||
formatter = "yyyy-MM-dd HH:mm";
|
||||
}
|
||||
|
||||
DateTimeFormatter dateTimeFormatter = dateFormatterCache.get(formatter);
|
||||
LocalDateTime dateTime = LocalDateTime.parse(datetimeStr, dateTimeFormatter);
|
||||
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} else if (datetimeStr.contains("-")) {
|
||||
// 只包含日期
|
||||
String formatter = "yyyy-MM-dd";
|
||||
DateTimeFormatter dateTimeFormatter = dateFormatterCache.get(formatter);
|
||||
LocalDate localDate = LocalDate.parse(datetimeStr, dateTimeFormatter);
|
||||
return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
|
||||
} else if (datetimeStr.contains(":")) {
|
||||
// 只包含时间
|
||||
String formatter;
|
||||
if (datetimeStr.contains(".")) {
|
||||
// 包含毫秒
|
||||
int msLen = len - datetimeStr.indexOf(".") - 1;
|
||||
StringBuilder ms = new StringBuilder();
|
||||
for (int i = 0; i < msLen; i++) {
|
||||
ms.append("S");
|
||||
}
|
||||
formatter = "HH:mm:ss." + ms;
|
||||
} else {
|
||||
// 判断包含时间位数
|
||||
int i = datetimeStr.indexOf(":");
|
||||
i = datetimeStr.indexOf(":", i + 1);
|
||||
if (i > -1) {
|
||||
formatter = "HH:mm:ss";
|
||||
} else {
|
||||
formatter = "HH:mm";
|
||||
}
|
||||
}
|
||||
DateTimeFormatter dateTimeFormatter = dateFormatterCache.get(formatter);
|
||||
LocalTime localTime = LocalTime.parse(datetimeStr, dateTimeFormatter);
|
||||
LocalDate localDate = LocalDate.of(1970, Month.JANUARY, 1);
|
||||
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
95
client-adapter/elasticsearch/pom.xml
Normal file
95
client-adapter/elasticsearch/pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.elasticsearch</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>canal client adapter elasticsearch module for otter ${project.version}</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastsql</groupId>
|
||||
<artifactId>fastsql</artifactId>
|
||||
<version>2.0.0_preview_855</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.elasticsearch</groupId>
|
||||
<artifactId>elasticsearch</artifactId>
|
||||
<version>6.2.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.elasticsearch.client</groupId>
|
||||
<artifactId>transport</artifactId>
|
||||
<version>6.2.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.40</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<copy todir="${project.basedir}/../launcher/target/classes/es" overwrite="true">
|
||||
<fileset dir="${project.basedir}/target/classes/es" erroronmissingdir="true">
|
||||
<include name="*.yml" />
|
||||
</fileset>
|
||||
</copy>
|
||||
</tasks>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,260 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.transport.client.PreBuiltTransportClient;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.otter.canal.client.adapter.OuterAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfigLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SqlParser;
|
||||
import com.alibaba.otter.canal.client.adapter.es.monitor.ESConfigMonitor;
|
||||
import com.alibaba.otter.canal.client.adapter.es.service.ESEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.es.service.ESSyncService;
|
||||
import com.alibaba.otter.canal.client.adapter.es.support.ESTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.OuterAdapterConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.SPI;
|
||||
|
||||
|
||||
/**
|
||||
* ES外部适配器
|
||||
*
|
||||
* @author rewerma 2018-10-20
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@SPI("es")
|
||||
public class ESAdapter implements OuterAdapter {
|
||||
|
||||
private Map<String, ESSyncConfig> esSyncConfig = new ConcurrentHashMap<>(); // 文件名对应配置
|
||||
private Map<String, Map<String, ESSyncConfig>> dbTableEsSyncConfig = new ConcurrentHashMap<>(); // schema-table对应配置
|
||||
|
||||
private TransportClient transportClient;
|
||||
|
||||
private ESSyncService esSyncService;
|
||||
|
||||
private ESConfigMonitor esConfigMonitor;
|
||||
|
||||
private Properties envProperties;
|
||||
|
||||
public TransportClient getTransportClient() {
|
||||
return transportClient;
|
||||
}
|
||||
|
||||
public ESSyncService getEsSyncService() {
|
||||
return esSyncService;
|
||||
}
|
||||
|
||||
public Map<String, ESSyncConfig> getEsSyncConfig() {
|
||||
return esSyncConfig;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, ESSyncConfig>> getDbTableEsSyncConfig() {
|
||||
return dbTableEsSyncConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||
try {
|
||||
this.envProperties = envProperties;
|
||||
Map<String, ESSyncConfig> esSyncConfigTmp = ESSyncConfigLoader.load(envProperties);
|
||||
// 过滤不匹配的key的配置
|
||||
esSyncConfigTmp.forEach((key, config) -> {
|
||||
if ((config.getOuterAdapterKey() == null && configuration.getKey() == null)
|
||||
|| (config.getOuterAdapterKey() != null
|
||||
&& config.getOuterAdapterKey().equalsIgnoreCase(configuration.getKey()))) {
|
||||
esSyncConfig.put(key, config);
|
||||
}
|
||||
});
|
||||
|
||||
for (Map.Entry<String, ESSyncConfig> entry : esSyncConfig.entrySet()) {
|
||||
String configName = entry.getKey();
|
||||
ESSyncConfig config = entry.getValue();
|
||||
SchemaItem schemaItem = SqlParser.parse(config.getEsMapping().getSql());
|
||||
config.getEsMapping().setSchemaItem(schemaItem);
|
||||
|
||||
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (dataSource == null || dataSource.getUrl() == null) {
|
||||
throw new RuntimeException("No data source found: " + config.getDataSourceKey());
|
||||
}
|
||||
Pattern pattern = Pattern.compile(".*:(.*)://.*/(.*)\\?.*$");
|
||||
Matcher matcher = pattern.matcher(dataSource.getUrl());
|
||||
if (!matcher.find()) {
|
||||
throw new RuntimeException("Not found the schema of jdbc-url: " + config.getDataSourceKey());
|
||||
}
|
||||
String schema = matcher.group(2);
|
||||
|
||||
schemaItem.getAliasTableItems().values().forEach(tableItem -> {
|
||||
Map<String, ESSyncConfig> esSyncConfigMap;
|
||||
if (envProperties != null
|
||||
&& !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||
esSyncConfigMap = dbTableEsSyncConfig
|
||||
.computeIfAbsent(StringUtils.trimToEmpty(config.getDestination()) + "-"
|
||||
+ StringUtils.trimToEmpty(config.getGroupId()) + "_" + schema + "-"
|
||||
+ tableItem.getTableName(),
|
||||
k -> new ConcurrentHashMap<>());
|
||||
} else {
|
||||
esSyncConfigMap = dbTableEsSyncConfig
|
||||
.computeIfAbsent(StringUtils.trimToEmpty(config.getDestination()) + "_" + schema + "-"
|
||||
+ tableItem.getTableName(),
|
||||
k -> new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
esSyncConfigMap.put(configName, config);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, String> properties = configuration.getProperties();
|
||||
Settings.Builder settingBuilder = Settings.builder();
|
||||
properties.forEach(settingBuilder::put);
|
||||
Settings settings = settingBuilder.build();
|
||||
transportClient = new PreBuiltTransportClient(settings);
|
||||
String[] hostArray = configuration.getHosts().split(",");
|
||||
for (String host : hostArray) {
|
||||
int i = host.indexOf(":");
|
||||
transportClient.addTransportAddress(new TransportAddress(InetAddress.getByName(host.substring(0, i)),
|
||||
Integer.parseInt(host.substring(i + 1))));
|
||||
}
|
||||
ESTemplate esTemplate = new ESTemplate(transportClient);
|
||||
esSyncService = new ESSyncService(esTemplate);
|
||||
|
||||
esConfigMonitor = new ESConfigMonitor();
|
||||
esConfigMonitor.init(this, envProperties);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sync(List<Dml> dmls) {
|
||||
if (dmls == null || dmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Dml dml : dmls) {
|
||||
if (!dml.getIsDdl()) {
|
||||
sync(dml);
|
||||
}
|
||||
}
|
||||
esSyncService.commit(); // 批次统一提交
|
||||
|
||||
}
|
||||
|
||||
private void sync(Dml dml) {
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> configMap;
|
||||
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||
configMap = dbTableEsSyncConfig
|
||||
.get(StringUtils.trimToEmpty(dml.getDestination()) + "-" + StringUtils.trimToEmpty(dml.getGroupId())
|
||||
+ "_" + database + "-" + table);
|
||||
} else {
|
||||
configMap = dbTableEsSyncConfig
|
||||
.get(StringUtils.trimToEmpty(dml.getDestination()) + "_" + database + "-" + table);
|
||||
}
|
||||
|
||||
if (configMap != null && !configMap.values().isEmpty()) {
|
||||
esSyncService.sync(configMap.values(), dml);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EtlResult etl(String task, List<String> params) {
|
||||
EtlResult etlResult = new EtlResult();
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
if (config != null) {
|
||||
DataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
ESEtlService esEtlService = new ESEtlService(transportClient, config);
|
||||
if (dataSource != null) {
|
||||
return esEtlService.importData(params);
|
||||
} else {
|
||||
etlResult.setSucceeded(false);
|
||||
etlResult.setErrorMessage("DataSource not found");
|
||||
return etlResult;
|
||||
}
|
||||
} else {
|
||||
StringBuilder resultMsg = new StringBuilder();
|
||||
boolean resSuccess = true;
|
||||
// ds不为空说明传入的是datasourceKey
|
||||
for (ESSyncConfig configTmp : esSyncConfig.values()) {
|
||||
// 取所有的destination为task的配置
|
||||
if (configTmp.getDestination().equals(task)) {
|
||||
ESEtlService esEtlService = new ESEtlService(transportClient, configTmp);
|
||||
EtlResult etlRes = esEtlService.importData(params);
|
||||
if (!etlRes.getSucceeded()) {
|
||||
resSuccess = false;
|
||||
resultMsg.append(etlRes.getErrorMessage()).append("\n");
|
||||
} else {
|
||||
resultMsg.append(etlRes.getResultMessage()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resultMsg.length() > 0) {
|
||||
etlResult.setSucceeded(resSuccess);
|
||||
if (resSuccess) {
|
||||
etlResult.setResultMessage(resultMsg.toString());
|
||||
} else {
|
||||
etlResult.setErrorMessage(resultMsg.toString());
|
||||
}
|
||||
return etlResult;
|
||||
}
|
||||
}
|
||||
etlResult.setSucceeded(false);
|
||||
etlResult.setErrorMessage("Task not found");
|
||||
return etlResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> count(String task) {
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
SearchResponse response = transportClient.prepareSearch(mapping.get_index())
|
||||
.setTypes(mapping.get_type())
|
||||
.setSize(0)
|
||||
.get();
|
||||
|
||||
long rowCount = response.getHits().getTotalHits();
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("esIndex", mapping.get_index());
|
||||
res.put("count", rowCount);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (esConfigMonitor != null) {
|
||||
esConfigMonitor.destroy();
|
||||
}
|
||||
if (transportClient != null) {
|
||||
transportClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDestination(String task) {
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
if (config != null) {
|
||||
return config.getDestination();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ES 映射配置
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESSyncConfig {
|
||||
|
||||
private String dataSourceKey; // 数据源key
|
||||
|
||||
private String outerAdapterKey; // adapter key
|
||||
|
||||
private String groupId; // group id
|
||||
|
||||
private String destination; // canal destination
|
||||
|
||||
private ESMapping esMapping;
|
||||
|
||||
public void validate() {
|
||||
if (esMapping._index == null) {
|
||||
throw new NullPointerException("esMapping._index");
|
||||
}
|
||||
if (esMapping._type == null) {
|
||||
throw new NullPointerException("esMapping._type");
|
||||
}
|
||||
if (esMapping._id == null && esMapping.getPk() == null) {
|
||||
throw new NullPointerException("esMapping._id or esMapping.pk");
|
||||
}
|
||||
if (esMapping.sql == null) {
|
||||
throw new NullPointerException("esMapping.sql");
|
||||
}
|
||||
}
|
||||
|
||||
public String getDataSourceKey() {
|
||||
return dataSourceKey;
|
||||
}
|
||||
|
||||
public void setDataSourceKey(String dataSourceKey) {
|
||||
this.dataSourceKey = dataSourceKey;
|
||||
}
|
||||
|
||||
public String getOuterAdapterKey() {
|
||||
return outerAdapterKey;
|
||||
}
|
||||
|
||||
public void setOuterAdapterKey(String outerAdapterKey) {
|
||||
this.outerAdapterKey = outerAdapterKey;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public ESMapping getEsMapping() {
|
||||
return esMapping;
|
||||
}
|
||||
|
||||
public void setEsMapping(ESMapping esMapping) {
|
||||
this.esMapping = esMapping;
|
||||
}
|
||||
|
||||
public static class ESMapping {
|
||||
|
||||
private String _index;
|
||||
private String _type;
|
||||
private String _id;
|
||||
private boolean upsert = false;
|
||||
private String pk;
|
||||
private Map<String, RelationMapping> relations = new LinkedHashMap<>();
|
||||
private String sql;
|
||||
// 对象字段, 例: objFields:
|
||||
// - _labels: array:;
|
||||
private Map<String, String> objFields = new LinkedHashMap<>();
|
||||
private List<String> skips = new ArrayList<>();
|
||||
private int commitBatch = 1000;
|
||||
private String etlCondition;
|
||||
private boolean syncByTimestamp = false; // 是否按时间戳定时同步
|
||||
private Long syncInterval; // 同步时间间隔
|
||||
|
||||
private SchemaItem schemaItem; // sql解析结果模型
|
||||
|
||||
public String get_index() {
|
||||
return _index;
|
||||
}
|
||||
|
||||
public void set_index(String _index) {
|
||||
this._index = _index;
|
||||
}
|
||||
|
||||
public String get_type() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public void set_type(String _type) {
|
||||
this._type = _type;
|
||||
}
|
||||
|
||||
public String get_id() {
|
||||
return _id;
|
||||
}
|
||||
|
||||
public void set_id(String _id) {
|
||||
this._id = _id;
|
||||
}
|
||||
|
||||
public boolean isUpsert() {
|
||||
return upsert;
|
||||
}
|
||||
|
||||
public void setUpsert(boolean upsert) {
|
||||
this.upsert = upsert;
|
||||
}
|
||||
|
||||
public String getPk() {
|
||||
return pk;
|
||||
}
|
||||
|
||||
public void setPk(String pk) {
|
||||
this.pk = pk;
|
||||
}
|
||||
|
||||
public Map<String, String> getObjFields() {
|
||||
return objFields;
|
||||
}
|
||||
|
||||
public void setObjFields(Map<String, String> objFields) {
|
||||
this.objFields = objFields;
|
||||
}
|
||||
|
||||
public List<String> getSkips() {
|
||||
return skips;
|
||||
}
|
||||
|
||||
public void setSkips(List<String> skips) {
|
||||
this.skips = skips;
|
||||
}
|
||||
|
||||
public Map<String, RelationMapping> getRelations() {
|
||||
return relations;
|
||||
}
|
||||
|
||||
public void setRelations(Map<String, RelationMapping> relations) {
|
||||
this.relations = relations;
|
||||
}
|
||||
|
||||
public String getSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public void setSql(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
public int getCommitBatch() {
|
||||
return commitBatch;
|
||||
}
|
||||
|
||||
public void setCommitBatch(int commitBatch) {
|
||||
this.commitBatch = commitBatch;
|
||||
}
|
||||
|
||||
public String getEtlCondition() {
|
||||
return etlCondition;
|
||||
}
|
||||
|
||||
public void setEtlCondition(String etlCondition) {
|
||||
this.etlCondition = etlCondition;
|
||||
}
|
||||
|
||||
public Long getSyncInterval() {
|
||||
return syncInterval;
|
||||
}
|
||||
|
||||
public void setSyncInterval(Long syncInterval) {
|
||||
this.syncInterval = syncInterval;
|
||||
}
|
||||
|
||||
public boolean isSyncByTimestamp() {
|
||||
return syncByTimestamp;
|
||||
}
|
||||
|
||||
public void setSyncByTimestamp(boolean syncByTimestamp) {
|
||||
this.syncByTimestamp = syncByTimestamp;
|
||||
}
|
||||
|
||||
public SchemaItem getSchemaItem() {
|
||||
return schemaItem;
|
||||
}
|
||||
|
||||
public void setSchemaItem(SchemaItem schemaItem) {
|
||||
this.schemaItem = schemaItem;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RelationMapping {
|
||||
|
||||
private String name;
|
||||
private String parent;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void setParent(String parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||
|
||||
/**
|
||||
* ES 配置装载器
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESSyncConfigLoader {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(ESSyncConfigLoader.class);
|
||||
|
||||
public static synchronized Map<String, ESSyncConfig> load(Properties envProperties) {
|
||||
logger.info("## Start loading es mapping config ... ");
|
||||
|
||||
Map<String, ESSyncConfig> esSyncConfig = new LinkedHashMap<>();
|
||||
|
||||
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("es");
|
||||
configContentMap.forEach((fileName, content) -> {
|
||||
ESSyncConfig config = YmlConfigBinder.bindYmlToObj(null, content, ESSyncConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
config.validate();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("ERROR Config: " + fileName + " " + e.getMessage(), e);
|
||||
}
|
||||
esSyncConfig.put(fileName, config);
|
||||
});
|
||||
|
||||
logger.info("## ES mapping config loaded");
|
||||
return esSyncConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig.ESMapping;
|
||||
|
||||
/**
|
||||
* ES 映射配置视图
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class SchemaItem {
|
||||
|
||||
private Map<String, TableItem> aliasTableItems = new LinkedHashMap<>(); // 别名对应表名
|
||||
private Map<String, FieldItem> selectFields = new LinkedHashMap<>(); // 查询字段
|
||||
private String sql;
|
||||
|
||||
private volatile Map<String, List<TableItem>> tableItemAliases;
|
||||
private volatile Map<String, List<FieldItem>> columnFields;
|
||||
private volatile Boolean allFieldsSimple;
|
||||
|
||||
public void init() {
|
||||
this.getTableItemAliases();
|
||||
this.getColumnFields();
|
||||
this.isAllFieldsSimple();
|
||||
aliasTableItems.values().forEach(tableItem -> {
|
||||
tableItem.getRelationTableFields();
|
||||
tableItem.getRelationSelectFieldItems();
|
||||
});
|
||||
}
|
||||
|
||||
public Map<String, TableItem> getAliasTableItems() {
|
||||
return aliasTableItems;
|
||||
}
|
||||
|
||||
public void setAliasTableItems(Map<String, TableItem> aliasTableItems) {
|
||||
this.aliasTableItems = aliasTableItems;
|
||||
}
|
||||
|
||||
public String getSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public void setSql(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
public Map<String, FieldItem> getSelectFields() {
|
||||
return selectFields;
|
||||
}
|
||||
|
||||
public void setSelectFields(Map<String, FieldItem> selectFields) {
|
||||
this.selectFields = selectFields;
|
||||
}
|
||||
|
||||
public String toSql() {
|
||||
// todo
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, List<TableItem>> getTableItemAliases() {
|
||||
if (tableItemAliases == null) {
|
||||
synchronized (SchemaItem.class) {
|
||||
if (tableItemAliases == null) {
|
||||
tableItemAliases = new LinkedHashMap<>();
|
||||
aliasTableItems.forEach((alias, tableItem) -> {
|
||||
List<TableItem> aliases = tableItemAliases
|
||||
.computeIfAbsent(tableItem.getTableName().toLowerCase(), k -> new ArrayList<>());
|
||||
aliases.add(tableItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return tableItemAliases;
|
||||
}
|
||||
|
||||
public Map<String, List<FieldItem>> getColumnFields() {
|
||||
if (columnFields == null) {
|
||||
synchronized (SchemaItem.class) {
|
||||
if (columnFields == null) {
|
||||
columnFields = new LinkedHashMap<>();
|
||||
getSelectFields()
|
||||
.forEach((fieldName, fieldItem) -> fieldItem.getColumnItems().forEach(columnItem -> {
|
||||
// TableItem tableItem = getAliasTableItems().get(columnItem.getOwner());
|
||||
// if (!tableItem.isSubQuery()) {
|
||||
List<FieldItem> fieldItems = columnFields.computeIfAbsent(
|
||||
columnItem.getOwner() + "." + columnItem.getColumnName(),
|
||||
k -> new ArrayList<>());
|
||||
fieldItems.add(fieldItem);
|
||||
// } else {
|
||||
// tableItem.getSubQueryFields().forEach(subQueryField -> {
|
||||
// List<FieldItem> fieldItems = columnFields.computeIfAbsent(
|
||||
// columnItem.getOwner() + "." + subQueryField.getColumn().getColumnName(),
|
||||
// k -> new ArrayList<>());
|
||||
// fieldItems.add(fieldItem);
|
||||
// });
|
||||
// }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
return columnFields;
|
||||
}
|
||||
|
||||
public boolean isAllFieldsSimple() {
|
||||
if (allFieldsSimple == null) {
|
||||
synchronized (SchemaItem.class) {
|
||||
if (allFieldsSimple == null) {
|
||||
allFieldsSimple = true;
|
||||
|
||||
for (FieldItem fieldItem : getSelectFields().values()) {
|
||||
if (fieldItem.isMethod() || fieldItem.isBinaryOp()) {
|
||||
allFieldsSimple = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allFieldsSimple;
|
||||
}
|
||||
|
||||
public TableItem getMainTable() {
|
||||
if (!aliasTableItems.isEmpty()) {
|
||||
return aliasTableItems.values().iterator().next();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public FieldItem getIdFieldItem(ESMapping mapping) {
|
||||
if (mapping.get_id() != null) {
|
||||
return getSelectFields().get(mapping.get_id());
|
||||
} else {
|
||||
return getSelectFields().get(mapping.getPk());
|
||||
}
|
||||
}
|
||||
|
||||
public static class TableItem {
|
||||
|
||||
private SchemaItem schemaItem;
|
||||
|
||||
private String schema;
|
||||
private String tableName;
|
||||
private String alias;
|
||||
private String subQuerySql;
|
||||
private List<FieldItem> subQueryFields = new ArrayList<>();
|
||||
private List<RelationFieldsPair> relationFields = new ArrayList<>();
|
||||
|
||||
private boolean main;
|
||||
private boolean subQuery;
|
||||
|
||||
private volatile Map<FieldItem, List<FieldItem>> relationTableFields; // 当前表关联条件字段对应主表查询字段
|
||||
private volatile List<FieldItem> relationSelectFieldItems; // 子表所在主表的查询字段
|
||||
|
||||
public TableItem(SchemaItem schemaItem){
|
||||
this.schemaItem = schemaItem;
|
||||
}
|
||||
|
||||
public SchemaItem getSchemaItem() {
|
||||
return schemaItem;
|
||||
}
|
||||
|
||||
public void setSchemaItem(SchemaItem schemaItem) {
|
||||
this.schemaItem = schemaItem;
|
||||
}
|
||||
|
||||
public String getSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
public void setSchema(String schema) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
public String getTableName() {
|
||||
return tableName;
|
||||
}
|
||||
|
||||
public void setTableName(String tableName) {
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public void setAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public String getSubQuerySql() {
|
||||
return subQuerySql;
|
||||
}
|
||||
|
||||
public void setSubQuerySql(String subQuerySql) {
|
||||
this.subQuerySql = subQuerySql;
|
||||
}
|
||||
|
||||
public boolean isMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
public void setMain(boolean main) {
|
||||
this.main = main;
|
||||
}
|
||||
|
||||
public boolean isSubQuery() {
|
||||
return subQuery;
|
||||
}
|
||||
|
||||
public void setSubQuery(boolean subQuery) {
|
||||
this.subQuery = subQuery;
|
||||
}
|
||||
|
||||
public List<FieldItem> getSubQueryFields() {
|
||||
return subQueryFields;
|
||||
}
|
||||
|
||||
public void setSubQueryFields(List<FieldItem> subQueryFields) {
|
||||
this.subQueryFields = subQueryFields;
|
||||
}
|
||||
|
||||
public List<RelationFieldsPair> getRelationFields() {
|
||||
return relationFields;
|
||||
}
|
||||
|
||||
public void setRelationFields(List<RelationFieldsPair> relationFields) {
|
||||
this.relationFields = relationFields;
|
||||
}
|
||||
|
||||
public Map<FieldItem, List<FieldItem>> getRelationTableFields() {
|
||||
if (relationTableFields == null) {
|
||||
synchronized (SchemaItem.class) {
|
||||
if (relationTableFields == null) {
|
||||
relationTableFields = new LinkedHashMap<>();
|
||||
|
||||
getRelationFields().forEach(relationFieldsPair -> {
|
||||
FieldItem leftFieldItem = relationFieldsPair.getLeftFieldItem();
|
||||
FieldItem rightFieldItem = relationFieldsPair.getRightFieldItem();
|
||||
FieldItem currentTableRelField = null;
|
||||
if (getAlias().equals(leftFieldItem.getOwner())) {
|
||||
currentTableRelField = leftFieldItem;
|
||||
} else if (getAlias().equals(rightFieldItem.getOwner())) {
|
||||
currentTableRelField = rightFieldItem;
|
||||
}
|
||||
|
||||
if (currentTableRelField != null) {
|
||||
List<FieldItem> selectFieldItem = getSchemaItem().getColumnFields()
|
||||
.get(leftFieldItem.getOwner() + "." + leftFieldItem.getColumn().getColumnName());
|
||||
if (selectFieldItem != null && !selectFieldItem.isEmpty()) {
|
||||
relationTableFields.put(currentTableRelField, selectFieldItem);
|
||||
} else {
|
||||
selectFieldItem = getSchemaItem().getColumnFields()
|
||||
.get(rightFieldItem.getOwner() + "."
|
||||
+ rightFieldItem.getColumn().getColumnName());
|
||||
if (selectFieldItem != null && !selectFieldItem.isEmpty()) {
|
||||
relationTableFields.put(currentTableRelField, selectFieldItem);
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Relation condition column must in select columns.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return relationTableFields;
|
||||
}
|
||||
|
||||
public List<FieldItem> getRelationSelectFieldItems() {
|
||||
if (relationSelectFieldItems == null) {
|
||||
synchronized (SchemaItem.class) {
|
||||
if (relationSelectFieldItems == null) {
|
||||
List<FieldItem> relationSelectFieldItemsTmp = new ArrayList<>();
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
if (fieldItem.getOwners().contains(getAlias())) {
|
||||
relationSelectFieldItemsTmp.add(fieldItem);
|
||||
}
|
||||
}
|
||||
relationSelectFieldItems = relationSelectFieldItemsTmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return relationSelectFieldItems;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RelationFieldsPair {
|
||||
|
||||
private FieldItem leftFieldItem;
|
||||
private FieldItem rightFieldItem;
|
||||
|
||||
public RelationFieldsPair(FieldItem leftFieldItem, FieldItem rightFieldItem){
|
||||
this.leftFieldItem = leftFieldItem;
|
||||
this.rightFieldItem = rightFieldItem;
|
||||
}
|
||||
|
||||
public FieldItem getLeftFieldItem() {
|
||||
return leftFieldItem;
|
||||
}
|
||||
|
||||
public void setLeftFieldItem(FieldItem leftFieldItem) {
|
||||
this.leftFieldItem = leftFieldItem;
|
||||
}
|
||||
|
||||
public FieldItem getRightFieldItem() {
|
||||
return rightFieldItem;
|
||||
}
|
||||
|
||||
public void setRightFieldItem(FieldItem rightFieldItem) {
|
||||
this.rightFieldItem = rightFieldItem;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FieldItem {
|
||||
|
||||
private String fieldName;
|
||||
private String expr;
|
||||
private List<ColumnItem> columnItems = new ArrayList<>();
|
||||
private List<String> owners = new ArrayList<>();
|
||||
|
||||
private boolean method;
|
||||
private boolean binaryOp;
|
||||
|
||||
public String getFieldName() {
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
public void setFieldName(String fieldName) {
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
public String getExpr() {
|
||||
return expr;
|
||||
}
|
||||
|
||||
public void setExpr(String expr) {
|
||||
this.expr = expr;
|
||||
}
|
||||
|
||||
public List<ColumnItem> getColumnItems() {
|
||||
return columnItems;
|
||||
}
|
||||
|
||||
public void setColumnItems(List<ColumnItem> columnItems) {
|
||||
this.columnItems = columnItems;
|
||||
}
|
||||
|
||||
public boolean isMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(boolean method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public boolean isBinaryOp() {
|
||||
return binaryOp;
|
||||
}
|
||||
|
||||
public void setBinaryOp(boolean binaryOp) {
|
||||
this.binaryOp = binaryOp;
|
||||
}
|
||||
|
||||
public List<String> getOwners() {
|
||||
return owners;
|
||||
}
|
||||
|
||||
public void setOwners(List<String> owners) {
|
||||
this.owners = owners;
|
||||
}
|
||||
|
||||
public void addColumn(ColumnItem columnItem) {
|
||||
columnItems.add(columnItem);
|
||||
}
|
||||
|
||||
public ColumnItem getColumn() {
|
||||
if (!columnItems.isEmpty()) {
|
||||
return columnItems.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
if (!owners.isEmpty()) {
|
||||
return owners.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
FieldItem fieldItem = (FieldItem) o;
|
||||
|
||||
return fieldName != null ? fieldName.equals(fieldItem.fieldName) : fieldItem.fieldName == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return fieldName != null ? fieldName.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ColumnItem {
|
||||
|
||||
private String owner;
|
||||
private String columnName;
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public String getColumnName() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
public void setColumnName(String columnName) {
|
||||
this.columnName = columnName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.config;
|
||||
|
||||
import static com.alibaba.fastsql.sql.ast.expr.SQLBinaryOperator.BooleanAnd;
|
||||
import static com.alibaba.fastsql.sql.ast.expr.SQLBinaryOperator.Equality;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.alibaba.fastsql.sql.SQLUtils;
|
||||
import com.alibaba.fastsql.sql.ast.SQLExpr;
|
||||
import com.alibaba.fastsql.sql.ast.expr.SQLBinaryOpExpr;
|
||||
import com.alibaba.fastsql.sql.ast.expr.SQLCaseExpr;
|
||||
import com.alibaba.fastsql.sql.ast.expr.SQLIdentifierExpr;
|
||||
import com.alibaba.fastsql.sql.ast.expr.SQLMethodInvokeExpr;
|
||||
import com.alibaba.fastsql.sql.ast.expr.SQLPropertyExpr;
|
||||
import com.alibaba.fastsql.sql.ast.statement.SQLExprTableSource;
|
||||
import com.alibaba.fastsql.sql.ast.statement.SQLJoinTableSource;
|
||||
import com.alibaba.fastsql.sql.ast.statement.SQLSelectStatement;
|
||||
import com.alibaba.fastsql.sql.ast.statement.SQLSubqueryTableSource;
|
||||
import com.alibaba.fastsql.sql.ast.statement.SQLTableSource;
|
||||
import com.alibaba.fastsql.sql.dialect.mysql.ast.statement.MySqlSelectQueryBlock;
|
||||
import com.alibaba.fastsql.sql.dialect.mysql.parser.MySqlStatementParser;
|
||||
import com.alibaba.fastsql.sql.parser.ParserException;
|
||||
import com.alibaba.fastsql.sql.parser.SQLStatementParser;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.ColumnItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.RelationFieldsPair;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.TableItem;
|
||||
|
||||
/**
|
||||
* ES同步指定sql格式解析
|
||||
*
|
||||
* @author rewerma 2018-10-26 下午03:45:49
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class SqlParser {
|
||||
|
||||
/**
|
||||
* 解析sql
|
||||
*
|
||||
* @param sql sql
|
||||
* @return 视图对象
|
||||
*/
|
||||
public static SchemaItem parse(String sql) {
|
||||
try {
|
||||
SQLStatementParser parser = new MySqlStatementParser(sql);
|
||||
SQLSelectStatement statement = (SQLSelectStatement) parser.parseStatement();
|
||||
MySqlSelectQueryBlock sqlSelectQueryBlock = (MySqlSelectQueryBlock) statement.getSelect().getQuery();
|
||||
|
||||
SchemaItem schemaItem = new SchemaItem();
|
||||
schemaItem.setSql(SQLUtils.toMySqlString(sqlSelectQueryBlock));
|
||||
SQLTableSource sqlTableSource = sqlSelectQueryBlock.getFrom();
|
||||
List<TableItem> tableItems = new ArrayList<>();
|
||||
SqlParser.visitSelectTable(schemaItem, sqlTableSource, tableItems, null);
|
||||
tableItems.forEach(tableItem -> schemaItem.getAliasTableItems().put(tableItem.getAlias(), tableItem));
|
||||
|
||||
List<FieldItem> fieldItems = collectSelectQueryFields(sqlSelectQueryBlock);
|
||||
fieldItems.forEach(fieldItem -> schemaItem.getSelectFields().put(fieldItem.getFieldName(), fieldItem));
|
||||
|
||||
schemaItem.init();
|
||||
|
||||
if (schemaItem.getAliasTableItems().isEmpty() || schemaItem.getSelectFields().isEmpty()) {
|
||||
throw new ParserException("Parse sql error");
|
||||
}
|
||||
return schemaItem;
|
||||
} catch (Exception e) {
|
||||
throw new ParserException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 归集字段
|
||||
*
|
||||
* @param sqlSelectQueryBlock sqlSelectQueryBlock
|
||||
* @return 字段属性列表
|
||||
*/
|
||||
private static List<FieldItem> collectSelectQueryFields(MySqlSelectQueryBlock sqlSelectQueryBlock) {
|
||||
return sqlSelectQueryBlock.getSelectList().stream().map(selectItem -> {
|
||||
FieldItem fieldItem = new FieldItem();
|
||||
fieldItem.setFieldName(selectItem.getAlias());
|
||||
fieldItem.setExpr(selectItem.toString());
|
||||
visitColumn(selectItem.getExpr(), fieldItem);
|
||||
return fieldItem;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析字段
|
||||
*
|
||||
* @param expr sql expr
|
||||
* @param fieldItem 字段属性
|
||||
*/
|
||||
private static void visitColumn(SQLExpr expr, FieldItem fieldItem) {
|
||||
if (expr instanceof SQLIdentifierExpr) {
|
||||
// 无owner
|
||||
SQLIdentifierExpr identifierExpr = (SQLIdentifierExpr) expr;
|
||||
if (fieldItem.getFieldName() == null) {
|
||||
fieldItem.setFieldName(identifierExpr.getName());
|
||||
fieldItem.setExpr(identifierExpr.toString());
|
||||
}
|
||||
ColumnItem columnItem = new ColumnItem();
|
||||
columnItem.setColumnName(identifierExpr.getName());
|
||||
fieldItem.getOwners().add(null);
|
||||
fieldItem.addColumn(columnItem);
|
||||
} else if (expr instanceof SQLPropertyExpr) {
|
||||
// 有owner
|
||||
SQLPropertyExpr sqlPropertyExpr = (SQLPropertyExpr) expr;
|
||||
if (fieldItem.getFieldName() == null) {
|
||||
fieldItem.setFieldName(sqlPropertyExpr.getName());
|
||||
fieldItem.setExpr(sqlPropertyExpr.toString());
|
||||
}
|
||||
fieldItem.getOwners().add(sqlPropertyExpr.getOwnernName());
|
||||
ColumnItem columnItem = new ColumnItem();
|
||||
columnItem.setColumnName(sqlPropertyExpr.getName());
|
||||
columnItem.setOwner(sqlPropertyExpr.getOwnernName());
|
||||
fieldItem.addColumn(columnItem);
|
||||
} else if (expr instanceof SQLMethodInvokeExpr) {
|
||||
SQLMethodInvokeExpr methodInvokeExpr = (SQLMethodInvokeExpr) expr;
|
||||
fieldItem.setMethod(true);
|
||||
for (SQLExpr sqlExpr : methodInvokeExpr.getArguments()) {
|
||||
visitColumn(sqlExpr, fieldItem);
|
||||
}
|
||||
} else if (expr instanceof SQLBinaryOpExpr) {
|
||||
SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) expr;
|
||||
fieldItem.setBinaryOp(true);
|
||||
visitColumn(sqlBinaryOpExpr.getLeft(), fieldItem);
|
||||
visitColumn(sqlBinaryOpExpr.getRight(), fieldItem);
|
||||
} else if (expr instanceof SQLCaseExpr) {
|
||||
SQLCaseExpr sqlCaseExpr = (SQLCaseExpr) expr;
|
||||
fieldItem.setMethod(true);
|
||||
sqlCaseExpr.getItems().forEach(item-> visitColumn(item.getConditionExpr(), fieldItem));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析表
|
||||
*
|
||||
* @param schemaItem 视图对象
|
||||
* @param sqlTableSource sqlTableSource
|
||||
* @param tableItems 表对象列表
|
||||
* @param tableItemTmp 表对象(临时)
|
||||
*/
|
||||
private static void visitSelectTable(SchemaItem schemaItem, SQLTableSource sqlTableSource,
|
||||
List<TableItem> tableItems, TableItem tableItemTmp) {
|
||||
if (sqlTableSource instanceof SQLExprTableSource) {
|
||||
SQLExprTableSource sqlExprTableSource = (SQLExprTableSource) sqlTableSource;
|
||||
TableItem tableItem;
|
||||
if (tableItemTmp != null) {
|
||||
tableItem = tableItemTmp;
|
||||
} else {
|
||||
tableItem = new TableItem(schemaItem);
|
||||
}
|
||||
tableItem.setSchema(sqlExprTableSource.getSchema());
|
||||
tableItem.setTableName(sqlExprTableSource.getTableName());
|
||||
if (tableItem.getAlias() == null) {
|
||||
tableItem.setAlias(sqlExprTableSource.getAlias());
|
||||
}
|
||||
if (tableItems.isEmpty()) {
|
||||
// 第一张表为主表
|
||||
tableItem.setMain(true);
|
||||
}
|
||||
tableItems.add(tableItem);
|
||||
} else if (sqlTableSource instanceof SQLJoinTableSource) {
|
||||
SQLJoinTableSource sqlJoinTableSource = (SQLJoinTableSource) sqlTableSource;
|
||||
SQLTableSource leftTableSource = sqlJoinTableSource.getLeft();
|
||||
visitSelectTable(schemaItem, leftTableSource, tableItems, null);
|
||||
SQLTableSource rightTableSource = sqlJoinTableSource.getRight();
|
||||
TableItem rightTableItem = new TableItem(schemaItem);
|
||||
// 解析on条件字段
|
||||
visitOnCondition(sqlJoinTableSource.getCondition(), rightTableItem);
|
||||
visitSelectTable(schemaItem, rightTableSource, tableItems, rightTableItem);
|
||||
|
||||
} else if (sqlTableSource instanceof SQLSubqueryTableSource) {
|
||||
SQLSubqueryTableSource subQueryTableSource = (SQLSubqueryTableSource) sqlTableSource;
|
||||
MySqlSelectQueryBlock sqlSelectQuery = (MySqlSelectQueryBlock) subQueryTableSource.getSelect().getQuery();
|
||||
TableItem tableItem;
|
||||
if (tableItemTmp != null) {
|
||||
tableItem = tableItemTmp;
|
||||
} else {
|
||||
tableItem = new TableItem(schemaItem);
|
||||
}
|
||||
tableItem.setAlias(subQueryTableSource.getAlias());
|
||||
tableItem.setSubQuerySql(SQLUtils.toMySqlString(sqlSelectQuery));
|
||||
tableItem.setSubQuery(true);
|
||||
tableItem.setSubQueryFields(collectSelectQueryFields(sqlSelectQuery));
|
||||
visitSelectTable(schemaItem, sqlSelectQuery.getFrom(), tableItems, tableItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析on条件
|
||||
*
|
||||
* @param expr sql expr
|
||||
* @param tableItem 表对象
|
||||
*/
|
||||
private static void visitOnCondition(SQLExpr expr, TableItem tableItem) {
|
||||
if (!(expr instanceof SQLBinaryOpExpr)) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
SQLBinaryOpExpr sqlBinaryOpExpr = (SQLBinaryOpExpr) expr;
|
||||
if (sqlBinaryOpExpr.getOperator() == BooleanAnd) {
|
||||
visitOnCondition(sqlBinaryOpExpr.getLeft(), tableItem);
|
||||
visitOnCondition(sqlBinaryOpExpr.getRight(), tableItem);
|
||||
} else if (sqlBinaryOpExpr.getOperator() == Equality) {
|
||||
FieldItem leftFieldItem = new FieldItem();
|
||||
visitColumn(sqlBinaryOpExpr.getLeft(), leftFieldItem);
|
||||
if (leftFieldItem.getColumnItems().size() != 1 || leftFieldItem.isMethod() || leftFieldItem.isBinaryOp()) {
|
||||
throw new UnsupportedOperationException("Unsupported for complex of on-condition");
|
||||
}
|
||||
FieldItem rightFieldItem = new FieldItem();
|
||||
visitColumn(sqlBinaryOpExpr.getRight(), rightFieldItem);
|
||||
if (rightFieldItem.getColumnItems().size() != 1 || rightFieldItem.isMethod()
|
||||
|| rightFieldItem.isBinaryOp()) {
|
||||
throw new UnsupportedOperationException("Unsupported for complex of on-condition");
|
||||
}
|
||||
tableItem.getRelationFields().add(new RelationFieldsPair(leftFieldItem, rightFieldItem));
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported for complex of on-condition");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.monitor;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.io.filefilter.FileFilterUtils;
|
||||
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
|
||||
import org.apache.commons.io.monitor.FileAlterationMonitor;
|
||||
import org.apache.commons.io.monitor.FileAlterationObserver;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SqlParser;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
public class ESConfigMonitor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ESConfigMonitor.class);
|
||||
|
||||
private static final String adapterName = "es";
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
private Properties envProperties;
|
||||
|
||||
private FileAlterationMonitor fileMonitor;
|
||||
|
||||
public void init(ESAdapter esAdapter, Properties envProperties) {
|
||||
this.esAdapter = esAdapter;
|
||||
this.envProperties = envProperties;
|
||||
File confDir = Util.getConfDirPath(adapterName);
|
||||
try {
|
||||
FileAlterationObserver observer = new FileAlterationObserver(confDir,
|
||||
FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter("yml")));
|
||||
FileListener listener = new FileListener();
|
||||
observer.addListener(listener);
|
||||
fileMonitor = new FileAlterationMonitor(3000, observer);
|
||||
fileMonitor.start();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
try {
|
||||
fileMonitor.stop();
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private class FileListener extends FileAlterationListenerAdaptor {
|
||||
|
||||
@Override
|
||||
public void onFileCreate(File file) {
|
||||
super.onFileCreate(file);
|
||||
try {
|
||||
// 加载新增的配置文件
|
||||
String configContent = MappingConfigsLoader.loadConfig(adapterName + File.separator + file.getName());
|
||||
ESSyncConfig config = YmlConfigBinder
|
||||
.bindYmlToObj(null, configContent, ESSyncConfig.class, null, envProperties);
|
||||
if (config != null) {
|
||||
config.validate();
|
||||
addConfigToCache(file, config);
|
||||
logger.info("Add a new es mapping config: {} to canal adapter", file.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileChange(File file) {
|
||||
super.onFileChange(file);
|
||||
|
||||
try {
|
||||
if (esAdapter.getEsSyncConfig().containsKey(file.getName())) {
|
||||
// 加载配置文件
|
||||
String configContent = MappingConfigsLoader
|
||||
.loadConfig(adapterName + File.separator + file.getName());
|
||||
if (configContent == null) {
|
||||
onFileDelete(file);
|
||||
return;
|
||||
}
|
||||
ESSyncConfig config = YmlConfigBinder
|
||||
.bindYmlToObj(null, configContent, ESSyncConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
config.validate();
|
||||
if (esAdapter.getEsSyncConfig().containsKey(file.getName())) {
|
||||
deleteConfigFromCache(file);
|
||||
}
|
||||
addConfigToCache(file, config);
|
||||
|
||||
logger.info("Change a es mapping config: {} of canal adapter", file.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileDelete(File file) {
|
||||
super.onFileDelete(file);
|
||||
|
||||
try {
|
||||
if (esAdapter.getEsSyncConfig().containsKey(file.getName())) {
|
||||
deleteConfigFromCache(file);
|
||||
|
||||
logger.info("Delete a es mapping config: {} of canal adapter", file.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addConfigToCache(File file, ESSyncConfig config) {
|
||||
esAdapter.getEsSyncConfig().put(file.getName(), config);
|
||||
|
||||
SchemaItem schemaItem = SqlParser.parse(config.getEsMapping().getSql());
|
||||
config.getEsMapping().setSchemaItem(schemaItem);
|
||||
|
||||
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (dataSource == null || dataSource.getUrl() == null) {
|
||||
throw new RuntimeException("No data source found: " + config.getDataSourceKey());
|
||||
}
|
||||
Pattern pattern = Pattern.compile(".*:(.*)://.*/(.*)\\?.*$");
|
||||
Matcher matcher = pattern.matcher(dataSource.getUrl());
|
||||
if (!matcher.find()) {
|
||||
throw new RuntimeException("Not found the schema of jdbc-url: " + config.getDataSourceKey());
|
||||
}
|
||||
String schema = matcher.group(2);
|
||||
|
||||
schemaItem.getAliasTableItems().values().forEach(tableItem -> {
|
||||
Map<String, ESSyncConfig> esSyncConfigMap = esAdapter.getDbTableEsSyncConfig()
|
||||
.computeIfAbsent(schema + "-" + tableItem.getTableName(), k -> new HashMap<>());
|
||||
esSyncConfigMap.put(file.getName(), config);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void deleteConfigFromCache(File file) {
|
||||
esAdapter.getEsSyncConfig().remove(file.getName());
|
||||
for (Map<String, ESSyncConfig> configMap : esAdapter.getDbTableEsSyncConfig().values()) {
|
||||
if (configMap != null) {
|
||||
configMap.remove(file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.service;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.action.update.UpdateRequestBuilder;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.support.ESTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
/**
|
||||
* ES ETL Service
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESEtlService {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(ESEtlService.class);
|
||||
|
||||
private TransportClient transportClient;
|
||||
private ESTemplate esTemplate;
|
||||
private ESSyncConfig config;
|
||||
|
||||
public ESEtlService(TransportClient transportClient, ESSyncConfig config){
|
||||
this.transportClient = transportClient;
|
||||
this.esTemplate = new ESTemplate(transportClient);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public EtlResult importData(List<String> params) {
|
||||
EtlResult etlResult = new EtlResult();
|
||||
AtomicLong impCount = new AtomicLong();
|
||||
List<String> errMsg = new ArrayList<>();
|
||||
String esIndex = "";
|
||||
if (config == null) {
|
||||
logger.warn("esSycnCofnig is null, etl go end !");
|
||||
etlResult.setErrorMessage("esSycnCofnig is null, etl go end !");
|
||||
return etlResult;
|
||||
}
|
||||
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
|
||||
esIndex = mapping.get_index();
|
||||
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
Pattern pattern = Pattern.compile(".*:(.*)://.*/(.*)\\?.*$");
|
||||
Matcher matcher = pattern.matcher(dataSource.getUrl());
|
||||
if (!matcher.find()) {
|
||||
throw new RuntimeException("Not found the schema of jdbc-url: " + config.getDataSourceKey());
|
||||
}
|
||||
String schema = matcher.group(2);
|
||||
|
||||
logger.info("etl from db: {}, to es index: {}", schema, esIndex);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
String sql = mapping.getSql();
|
||||
|
||||
// 拼接条件
|
||||
if (mapping.getEtlCondition() != null && params != null) {
|
||||
String etlCondition = mapping.getEtlCondition();
|
||||
int size = params.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
etlCondition = etlCondition.replace("{" + i + "}", params.get(i));
|
||||
}
|
||||
|
||||
sql += " " + etlCondition;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("etl sql : {}", mapping.getSql());
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
String countSql = "SELECT COUNT(1) FROM ( " + sql + ") _CNT ";
|
||||
long cnt = (Long) Util.sqlRS(dataSource, countSql, rs -> {
|
||||
Long count = null;
|
||||
try {
|
||||
if (rs.next()) {
|
||||
count = ((Number) rs.getObject(1)).longValue();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
return count == null ? 0L : count;
|
||||
});
|
||||
|
||||
// 当大于1万条记录时开启多线程
|
||||
if (cnt >= 10000) {
|
||||
int threadCount = 3; // 从配置读取默认为3
|
||||
long perThreadCnt = cnt / threadCount;
|
||||
ExecutorService executor = Util.newFixedThreadPool(threadCount, 5000L);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
long offset = i * perThreadCnt;
|
||||
Long size = null;
|
||||
if (i != threadCount - 1) {
|
||||
size = perThreadCnt;
|
||||
}
|
||||
String sqlFinal;
|
||||
if (size != null) {
|
||||
sqlFinal = sql + " LIMIT " + offset + "," + size;
|
||||
} else {
|
||||
sqlFinal = sql + " LIMIT " + offset + "," + cnt;
|
||||
}
|
||||
executor.execute(() -> executeSqlImport(dataSource, sqlFinal, mapping, impCount, errMsg));
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
while (!executor.awaitTermination(3, TimeUnit.SECONDS)) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
executeSqlImport(dataSource, sql, mapping, impCount, errMsg);
|
||||
}
|
||||
|
||||
logger.info("数据全量导入完成,一共导入 {} 条数据, 耗时: {}", impCount.get(), System.currentTimeMillis() - start);
|
||||
etlResult.setResultMessage("导入ES索引 " + esIndex + " 数据:" + impCount.get() + " 条");
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
errMsg.add(esIndex + " etl failed! ==>" + e.getMessage());
|
||||
}
|
||||
if (errMsg.isEmpty()) {
|
||||
etlResult.setSucceeded(true);
|
||||
} else {
|
||||
etlResult.setErrorMessage(Joiner.on("\n").join(errMsg));
|
||||
}
|
||||
return etlResult;
|
||||
}
|
||||
|
||||
private void processFailBulkResponse(BulkResponse bulkResponse) {
|
||||
for (BulkItemResponse response : bulkResponse.getItems()) {
|
||||
if (!response.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.warn(response.getFailureMessage());
|
||||
} else {
|
||||
logger.error("全量导入数据有误 {}", response.getFailureMessage());
|
||||
throw new RuntimeException("全量数据 etl 异常: " + response.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean executeSqlImport(DataSource ds, String sql, ESMapping mapping, AtomicLong impCount,
|
||||
List<String> errMsg) {
|
||||
try {
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
int count = 0;
|
||||
try {
|
||||
BulkRequestBuilder bulkRequestBuilder = transportClient.prepareBulk();
|
||||
|
||||
long batchBegin = System.currentTimeMillis();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
Object idVal = null;
|
||||
for (FieldItem fieldItem : mapping.getSchemaItem().getSelectFields().values()) {
|
||||
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
if (mapping.getSkips().contains(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是主键字段则不插入
|
||||
if (fieldItem.getFieldName().equals(mapping.get_id())) {
|
||||
idVal = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
} else {
|
||||
Object val = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
esFieldData.put(Util.cleanColumn(fieldName), val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!mapping.getRelations().isEmpty()) {
|
||||
mapping.getRelations().forEach((relationField, relationMapping) -> {
|
||||
Map<String, Object> relations = new HashMap<>();
|
||||
relations.put("name", relationMapping.getName());
|
||||
if (StringUtils.isNotEmpty(relationMapping.getParent())) {
|
||||
FieldItem parentFieldItem = mapping.getSchemaItem()
|
||||
.getSelectFields()
|
||||
.get(relationMapping.getParent());
|
||||
Object parentVal;
|
||||
try {
|
||||
parentVal = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
parentFieldItem.getFieldName(),
|
||||
parentFieldItem.getFieldName());
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (parentVal != null) {
|
||||
relations.put("parent", parentVal.toString());
|
||||
esFieldData.put("$parent_routing", parentVal.toString());
|
||||
|
||||
}
|
||||
}
|
||||
esFieldData.put(Util.cleanColumn(relationField), relations);
|
||||
});
|
||||
}
|
||||
|
||||
if (idVal != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
UpdateRequestBuilder updateRequestBuilder = transportClient
|
||||
.prepareUpdate(mapping.get_index(), mapping.get_type(), idVal.toString())
|
||||
.setDoc(esFieldData)
|
||||
.setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequestBuilder.setRouting(parentVal);
|
||||
}
|
||||
bulkRequestBuilder.add(updateRequestBuilder);
|
||||
} else {
|
||||
IndexRequestBuilder indexRequestBuilder = transportClient
|
||||
.prepareIndex(mapping.get_index(), mapping.get_type(), idVal.toString())
|
||||
.setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
indexRequestBuilder.setRouting(parentVal);
|
||||
}
|
||||
bulkRequestBuilder.add(indexRequestBuilder);
|
||||
}
|
||||
} else {
|
||||
idVal = esFieldData.get(mapping.getPk());
|
||||
SearchResponse response = transportClient.prepareSearch(mapping.get_index())
|
||||
.setTypes(mapping.get_type())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), idVal))
|
||||
.setSize(10000)
|
||||
.get();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
bulkRequestBuilder.add(
|
||||
transportClient.prepareUpdate(mapping.get_index(), mapping.get_type(), hit.getId())
|
||||
.setDoc(esFieldData));
|
||||
}
|
||||
}
|
||||
|
||||
if (bulkRequestBuilder.numberOfActions() % mapping.getCommitBatch() == 0
|
||||
&& bulkRequestBuilder.numberOfActions() > 0) {
|
||||
long esBatchBegin = System.currentTimeMillis();
|
||||
BulkResponse rp = bulkRequestBuilder.execute().actionGet();
|
||||
if (rp.hasFailures()) {
|
||||
this.processFailBulkResponse(rp);
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("全量数据批量导入批次耗时: {}, es执行时间: {}, 批次大小: {}, index; {}",
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
bulkRequestBuilder.numberOfActions(),
|
||||
mapping.get_index());
|
||||
}
|
||||
batchBegin = System.currentTimeMillis();
|
||||
bulkRequestBuilder = transportClient.prepareBulk();
|
||||
}
|
||||
count++;
|
||||
impCount.incrementAndGet();
|
||||
}
|
||||
|
||||
if (bulkRequestBuilder.numberOfActions() > 0) {
|
||||
long esBatchBegin = System.currentTimeMillis();
|
||||
BulkResponse rp = bulkRequestBuilder.execute().actionGet();
|
||||
if (rp.hasFailures()) {
|
||||
this.processFailBulkResponse(rp);
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("全量数据批量导入最后批次耗时: {}, es执行时间: {}, 批次大小: {}, index; {}",
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
bulkRequestBuilder.numberOfActions(),
|
||||
mapping.get_index());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
errMsg.add(mapping.get_index() + " etl failed! ==>" + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,845 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.ColumnItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.TableItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.support.ESSyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.es.support.ESTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
/**
|
||||
* ES 同步 Service
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESSyncService {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(ESSyncService.class);
|
||||
|
||||
private ESTemplate esTemplate;
|
||||
|
||||
public ESSyncService(ESTemplate esTemplate){
|
||||
this.esTemplate = esTemplate;
|
||||
}
|
||||
|
||||
public void sync(Collection<ESSyncConfig> esSyncConfigs, Dml dml) {
|
||||
long begin = System.currentTimeMillis();
|
||||
if (esSyncConfigs != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Destination: {}, database:{}, table:{}, type:{}, affected index count: {}",
|
||||
dml.getDestination(),
|
||||
dml.getDatabase(),
|
||||
dml.getTable(),
|
||||
dml.getType(),
|
||||
esSyncConfigs.size());
|
||||
}
|
||||
|
||||
for (ESSyncConfig config : esSyncConfigs) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Prepared to sync index: {}, destination: {}",
|
||||
config.getEsMapping().get_index(),
|
||||
dml.getDestination());
|
||||
}
|
||||
this.sync(config, dml);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sync completed: {}, destination: {}",
|
||||
config.getEsMapping().get_index(),
|
||||
dml.getDestination());
|
||||
}
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sync elapsed time: {} ms, affected indexes count:{}, destination: {}",
|
||||
(System.currentTimeMillis() - begin),
|
||||
esSyncConfigs.size(),
|
||||
dml.getDestination());
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
StringBuilder configIndexes = new StringBuilder();
|
||||
esSyncConfigs
|
||||
.forEach(esSyncConfig -> configIndexes.append(esSyncConfig.getEsMapping().get_index()).append(" "));
|
||||
logger.debug("DML: {} \nAffected indexes: {}",
|
||||
JSON.toJSONString(dml, SerializerFeature.WriteMapNullValue),
|
||||
configIndexes.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sync(ESSyncConfig config, Dml dml) {
|
||||
try {
|
||||
// 如果是按时间戳定时更新则返回
|
||||
if (config.getEsMapping().isSyncByTimestamp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long begin = System.currentTimeMillis();
|
||||
|
||||
String type = dml.getType();
|
||||
if (type != null && type.equalsIgnoreCase("INSERT")) {
|
||||
insert(config, dml);
|
||||
} else if (type != null && type.equalsIgnoreCase("UPDATE")) {
|
||||
update(config, dml);
|
||||
} else if (type != null && type.equalsIgnoreCase("DELETE")) {
|
||||
delete(config, dml);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sync elapsed time: {} ms,destination: {}, es index: {}",
|
||||
(System.currentTimeMillis() - begin),
|
||||
dml.getDestination(),
|
||||
config.getEsMapping().get_index());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.error("sync error, es index: {}, DML : {}", config.getEsMapping().get_index(), dml);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入操作dml
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml数据
|
||||
*/
|
||||
private void insert(ESSyncConfig config, Dml dml) {
|
||||
List<Map<String, Object>> dataList = dml.getData();
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SchemaItem schemaItem = config.getEsMapping().getSchemaItem();
|
||||
for (Map<String, Object> data : dataList) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (schemaItem.getAliasTableItems().size() == 1 && schemaItem.isAllFieldsSimple()) {
|
||||
// ------单表 & 所有字段都为简单字段------
|
||||
singleTableSimpleFiledInsert(config, dml, data);
|
||||
} else {
|
||||
// ------是主表 查询sql来插入------
|
||||
if (schemaItem.getMainTable().getTableName().equalsIgnoreCase(dml.getTable())) {
|
||||
mainTableInsert(config, dml, data);
|
||||
}
|
||||
|
||||
// 从表的操作
|
||||
for (TableItem tableItem : schemaItem.getAliasTableItems().values()) {
|
||||
if (tableItem.isMain()) {
|
||||
continue;
|
||||
}
|
||||
if (!tableItem.getTableName().equals(dml.getTable())) {
|
||||
continue;
|
||||
}
|
||||
// 关联条件出现在主表查询条件是否为简单字段
|
||||
boolean allFieldsSimple = true;
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
if (fieldItem.isMethod() || fieldItem.isBinaryOp()) {
|
||||
allFieldsSimple = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 所有查询字段均为简单字段
|
||||
if (allFieldsSimple) {
|
||||
// 不是子查询
|
||||
if (!tableItem.isSubQuery()) {
|
||||
// ------关联表简单字段插入------
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
Object value = esTemplate.getValFromData(config.getEsMapping(),
|
||||
data,
|
||||
fieldItem.getFieldName(),
|
||||
fieldItem.getColumn().getColumnName());
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
|
||||
joinTableSimpleFieldOperation(config, dml, data, tableItem, esFieldData);
|
||||
} else {
|
||||
// ------关联子表简单字段插入------
|
||||
subTableSimpleFieldOperation(config, dml, data, null, tableItem);
|
||||
}
|
||||
} else {
|
||||
// ------关联子表复杂字段插入 执行全sql更新es------
|
||||
wholeSqlOperation(config, dml, data, null, tableItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新操作dml
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml数据
|
||||
*/
|
||||
private void update(ESSyncConfig config, Dml dml) {
|
||||
List<Map<String, Object>> dataList = dml.getData();
|
||||
List<Map<String, Object>> oldList = dml.getOld();
|
||||
if (dataList == null || dataList.isEmpty() || oldList == null || oldList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SchemaItem schemaItem = config.getEsMapping().getSchemaItem();
|
||||
int i = 0;
|
||||
for (Map<String, Object> data : dataList) {
|
||||
Map<String, Object> old = oldList.get(i);
|
||||
if (data == null || data.isEmpty() || old == null || old.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (schemaItem.getAliasTableItems().size() == 1 && schemaItem.isAllFieldsSimple()) {
|
||||
// ------单表 & 所有字段都为简单字段------
|
||||
singleTableSimpleFiledUpdate(config, dml, data, old);
|
||||
} else {
|
||||
// ------主表 查询sql来更新------
|
||||
if (schemaItem.getMainTable().getTableName().equalsIgnoreCase(dml.getTable())) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
FieldItem idFieldItem = schemaItem.getSelectFields().get(idFieldName);
|
||||
|
||||
boolean idFieldSimple = true;
|
||||
if (idFieldItem.isMethod() || idFieldItem.isBinaryOp()) {
|
||||
idFieldSimple = false;
|
||||
}
|
||||
|
||||
boolean allUpdateFieldSimple = true;
|
||||
out: for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
for (ColumnItem columnItem : fieldItem.getColumnItems()) {
|
||||
if (old.containsKey(columnItem.getColumnName())) {
|
||||
if (fieldItem.isMethod() || fieldItem.isBinaryOp()) {
|
||||
allUpdateFieldSimple = false;
|
||||
break out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不支持主键更新!!
|
||||
|
||||
// 判断是否有外键更新
|
||||
boolean fkChanged = false;
|
||||
for (TableItem tableItem : schemaItem.getAliasTableItems().values()) {
|
||||
if (tableItem.isMain()) {
|
||||
continue;
|
||||
}
|
||||
boolean changed = false;
|
||||
for (List<FieldItem> fieldItems : tableItem.getRelationTableFields().values()) {
|
||||
for (FieldItem fieldItem : fieldItems) {
|
||||
if (old.containsKey(fieldItem.getColumn().getColumnName())) {
|
||||
fkChanged = true;
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果外键有修改,则更新所对应该表的所有查询条件数据
|
||||
if (changed) {
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
fieldItem.getColumnItems()
|
||||
.forEach(columnItem -> old.put(columnItem.getColumnName(), null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断主键和所更新的字段是否全为简单字段
|
||||
if (idFieldSimple && allUpdateFieldSimple && !fkChanged) {
|
||||
singleTableSimpleFiledUpdate(config, dml, data, old);
|
||||
} else {
|
||||
mainTableUpdate(config, dml, data, old);
|
||||
}
|
||||
}
|
||||
|
||||
// 从表的操作
|
||||
for (TableItem tableItem : schemaItem.getAliasTableItems().values()) {
|
||||
if (tableItem.isMain()) {
|
||||
continue;
|
||||
}
|
||||
if (!tableItem.getTableName().equals(dml.getTable())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 关联条件出现在主表查询条件是否为简单字段
|
||||
boolean allFieldsSimple = true;
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
if (fieldItem.isMethod() || fieldItem.isBinaryOp()) {
|
||||
allFieldsSimple = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有查询字段均为简单字段
|
||||
if (allFieldsSimple) {
|
||||
// 不是子查询
|
||||
if (!tableItem.isSubQuery()) {
|
||||
// ------关联表简单字段更新------
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
if (old.containsKey(fieldItem.getColumn().getColumnName())) {
|
||||
Object value = esTemplate.getValFromData(config.getEsMapping(),
|
||||
data,
|
||||
fieldItem.getFieldName(),
|
||||
fieldItem.getColumn().getColumnName());
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
}
|
||||
joinTableSimpleFieldOperation(config, dml, data, tableItem, esFieldData);
|
||||
} else {
|
||||
// ------关联子表简单字段更新------
|
||||
subTableSimpleFieldOperation(config, dml, data, old, tableItem);
|
||||
}
|
||||
} else {
|
||||
// ------关联子表复杂字段更新 执行全sql更新es------
|
||||
wholeSqlOperation(config, dml, data, old, tableItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作dml
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml数据
|
||||
*/
|
||||
private void delete(ESSyncConfig config, Dml dml) {
|
||||
List<Map<String, Object>> dataList = dml.getData();
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SchemaItem schemaItem = config.getEsMapping().getSchemaItem();
|
||||
|
||||
for (Map<String, Object> data : dataList) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
|
||||
// ------是主表------
|
||||
if (schemaItem.getMainTable().getTableName().equalsIgnoreCase(dml.getTable())) {
|
||||
if (mapping.get_id() != null) {
|
||||
FieldItem idFieldItem = schemaItem.getIdFieldItem(mapping);
|
||||
// 主键为简单字段
|
||||
if (!idFieldItem.isMethod() && !idFieldItem.isBinaryOp()) {
|
||||
Object idVal = esTemplate.getValFromData(mapping,
|
||||
data,
|
||||
idFieldItem.getFieldName(),
|
||||
idFieldItem.getColumn().getColumnName());
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Main table delete es index, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.delete(mapping, idVal, null);
|
||||
} else {
|
||||
// ------主键带函数, 查询sql获取主键删除------
|
||||
// FIXME 删除时反查sql为空记录, 无法获获取 id field 值
|
||||
mainTableDelete(config, dml, data);
|
||||
}
|
||||
} else {
|
||||
FieldItem pkFieldItem = schemaItem.getIdFieldItem(mapping);
|
||||
if (!pkFieldItem.isMethod() && !pkFieldItem.isBinaryOp()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
Object pkVal = esTemplate.getESDataFromDmlData(mapping, data, esFieldData);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Main table delete es index, destination:{}, table: {}, index: {}, pk: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
pkVal);
|
||||
}
|
||||
esFieldData.remove(pkFieldItem.getFieldName());
|
||||
esFieldData.keySet().forEach(key -> esFieldData.put(key, null));
|
||||
esTemplate.delete(mapping, pkVal, esFieldData);
|
||||
} else {
|
||||
// ------主键带函数, 查询sql获取主键删除------
|
||||
mainTableDelete(config, dml, data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 从表的操作
|
||||
for (TableItem tableItem : schemaItem.getAliasTableItems().values()) {
|
||||
if (tableItem.isMain()) {
|
||||
continue;
|
||||
}
|
||||
if (!tableItem.getTableName().equals(dml.getTable())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 关联条件出现在主表查询条件是否为简单字段
|
||||
boolean allFieldsSimple = true;
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
if (fieldItem.isMethod() || fieldItem.isBinaryOp()) {
|
||||
allFieldsSimple = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有查询字段均为简单字段
|
||||
if (allFieldsSimple) {
|
||||
// 不是子查询
|
||||
if (!tableItem.isSubQuery()) {
|
||||
// ------关联表简单字段更新为null------
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), null);
|
||||
}
|
||||
joinTableSimpleFieldOperation(config, dml, data, tableItem, esFieldData);
|
||||
} else {
|
||||
// ------关联子表简单字段更新------
|
||||
subTableSimpleFieldOperation(config, dml, data, null, tableItem);
|
||||
}
|
||||
} else {
|
||||
// ------关联子表复杂字段更新 执行全sql更新es------
|
||||
wholeSqlOperation(config, dml, data, null, tableItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表简单字段insert
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行dml数据
|
||||
*/
|
||||
private void singleTableSimpleFiledInsert(ESSyncConfig config, Dml dml, Map<String, Object> data) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
Object idVal = esTemplate.getESDataFromDmlData(mapping, data, esFieldData);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Single table insert ot es index, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.insert(mapping, idVal, esFieldData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主表(单表)复杂字段insert
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行dml数据
|
||||
*/
|
||||
private void mainTableInsert(ESSyncConfig config, Dml dml, Map<String, Object> data) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
String sql = mapping.getSql();
|
||||
String condition = ESSyncUtil.pkConditionSql(mapping, data);
|
||||
sql = ESSyncUtil.appendCondition(sql, condition);
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Main table insert ot es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
sql.replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
try {
|
||||
while (rs.next()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
Object idVal = esTemplate.getESDataFromRS(mapping, rs, esFieldData);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(
|
||||
"Main table insert ot es index by query sql, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.insert(mapping, idVal, esFieldData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
private void mainTableDelete(ESSyncConfig config, Dml dml, Map<String, Object> data) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
String sql = mapping.getSql();
|
||||
String condition = ESSyncUtil.pkConditionSql(mapping, data);
|
||||
sql = ESSyncUtil.appendCondition(sql, condition);
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Main table delete es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
sql.replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
try {
|
||||
Map<String, Object> esFieldData = null;
|
||||
if (mapping.getPk() != null) {
|
||||
esFieldData = new LinkedHashMap<>();
|
||||
esTemplate.getESDataFromDmlData(mapping, data, esFieldData);
|
||||
esFieldData.remove(mapping.getPk());
|
||||
for (String key : esFieldData.keySet()) {
|
||||
esFieldData.put(Util.cleanColumn(key), null);
|
||||
}
|
||||
}
|
||||
while (rs.next()) {
|
||||
Object idVal = esTemplate.getIdValFromRS(mapping, rs);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(
|
||||
"Main table delete ot es index by query sql, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.delete(mapping, idVal, esFieldData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联表主表简单字段operation
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行dml数据
|
||||
* @param tableItem 当前表配置
|
||||
*/
|
||||
private void joinTableSimpleFieldOperation(ESSyncConfig config, Dml dml, Map<String, Object> data,
|
||||
TableItem tableItem, Map<String, Object> esFieldData) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
|
||||
Map<String, Object> paramsTmp = new LinkedHashMap<>();
|
||||
for (Map.Entry<FieldItem, List<FieldItem>> entry : tableItem.getRelationTableFields().entrySet()) {
|
||||
for (FieldItem fieldItem : entry.getValue()) {
|
||||
if (fieldItem.getColumnItems().size() == 1) {
|
||||
Object value = esTemplate.getValFromData(mapping,
|
||||
data,
|
||||
fieldItem.getFieldName(),
|
||||
entry.getKey().getColumn().getColumnName());
|
||||
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
// 判断是否是主键
|
||||
if (fieldName.equals(mapping.get_id())) {
|
||||
fieldName = "_id";
|
||||
}
|
||||
paramsTmp.put(fieldName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.trace("Join table update es index by foreign key, destination:{}, table: {}, index: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index());
|
||||
}
|
||||
esTemplate.updateByQuery(config, paramsTmp, esFieldData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联子查询, 主表简单字段operation
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行dml数据
|
||||
* @param old 单行old数据
|
||||
* @param tableItem 当前表配置
|
||||
*/
|
||||
private void subTableSimpleFieldOperation(ESSyncConfig config, Dml dml, Map<String, Object> data,
|
||||
Map<String, Object> old, TableItem tableItem) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT * FROM (" + tableItem.getSubQuerySql() + ") " + tableItem.getAlias() + " WHERE ");
|
||||
|
||||
for (FieldItem fkFieldItem : tableItem.getRelationTableFields().keySet()) {
|
||||
String columnName = fkFieldItem.getColumn().getColumnName();
|
||||
Object value = esTemplate.getValFromData(mapping, data, fkFieldItem.getFieldName(), columnName);
|
||||
ESSyncUtil.appendCondition(sql, value, tableItem.getAlias(), columnName);
|
||||
}
|
||||
int len = sql.length();
|
||||
sql.delete(len - 5, len);
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Join table update es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
sql.toString().replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql.toString(), rs -> {
|
||||
try {
|
||||
while (rs.next()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
if (old != null) {
|
||||
out: for (FieldItem fieldItem1 : tableItem.getSubQueryFields()) {
|
||||
for (ColumnItem columnItem0 : fieldItem.getColumnItems()) {
|
||||
if (fieldItem1.getFieldName().equals(columnItem0.getColumnName()))
|
||||
for (ColumnItem columnItem : fieldItem1.getColumnItems()) {
|
||||
if (old.containsKey(columnItem.getColumnName())) {
|
||||
Object val = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
fieldItem.getFieldName(),
|
||||
fieldItem.getColumn().getColumnName());
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), val);
|
||||
break out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object val = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
fieldItem.getFieldName(),
|
||||
fieldItem.getColumn().getColumnName());
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), val);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> paramsTmp = new LinkedHashMap<>();
|
||||
for (Map.Entry<FieldItem, List<FieldItem>> entry : tableItem.getRelationTableFields().entrySet()) {
|
||||
for (FieldItem fieldItem : entry.getValue()) {
|
||||
if (fieldItem.getColumnItems().size() == 1) {
|
||||
Object value = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
fieldItem.getFieldName(),
|
||||
entry.getKey().getColumn().getColumnName());
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
// 判断是否是主键
|
||||
if (fieldName.equals(mapping.get_id())) {
|
||||
fieldName = "_id";
|
||||
}
|
||||
paramsTmp.put(fieldName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.trace("Join table update es index by query sql, destination:{}, table: {}, index: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index());
|
||||
}
|
||||
esTemplate.updateByQuery(config, paramsTmp, esFieldData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联(子查询), 主表复杂字段operation, 全sql执行
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行dml数据
|
||||
* @param tableItem 当前表配置
|
||||
*/
|
||||
private void wholeSqlOperation(ESSyncConfig config, Dml dml, Map<String, Object> data, Map<String, Object> old,
|
||||
TableItem tableItem) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
StringBuilder sql = new StringBuilder(mapping.getSql() + " WHERE ");
|
||||
|
||||
for (FieldItem fkFieldItem : tableItem.getRelationTableFields().keySet()) {
|
||||
String columnName = fkFieldItem.getColumn().getColumnName();
|
||||
Object value = esTemplate.getValFromData(mapping, data, fkFieldItem.getFieldName(), columnName);
|
||||
ESSyncUtil.appendCondition(sql, value, tableItem.getAlias(), columnName);
|
||||
}
|
||||
int len = sql.length();
|
||||
sql.delete(len - 5, len);
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Join table update es index by query whole sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
sql.toString().replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql.toString(), rs -> {
|
||||
try {
|
||||
while (rs.next()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
for (FieldItem fieldItem : tableItem.getRelationSelectFieldItems()) {
|
||||
if (old != null) {
|
||||
// 从表子查询
|
||||
out: for (FieldItem fieldItem1 : tableItem.getSubQueryFields()) {
|
||||
for (ColumnItem columnItem0 : fieldItem.getColumnItems()) {
|
||||
if (fieldItem1.getFieldName().equals(columnItem0.getColumnName()))
|
||||
for (ColumnItem columnItem : fieldItem1.getColumnItems()) {
|
||||
if (old.containsKey(columnItem.getColumnName())) {
|
||||
Object val = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
fieldItem.getFieldName(),
|
||||
fieldItem.getFieldName());
|
||||
esFieldData.put(fieldItem.getFieldName(), val);
|
||||
break out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 从表非子查询
|
||||
for (FieldItem fieldItem1 : tableItem.getRelationSelectFieldItems()) {
|
||||
if (fieldItem1.equals(fieldItem)) {
|
||||
for (ColumnItem columnItem : fieldItem1.getColumnItems()) {
|
||||
if (old.containsKey(columnItem.getColumnName())) {
|
||||
Object val = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
fieldItem.getFieldName(),
|
||||
fieldItem.getFieldName());
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object val = esTemplate
|
||||
.getValFromRS(mapping, rs, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), val);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> paramsTmp = new LinkedHashMap<>();
|
||||
for (Map.Entry<FieldItem, List<FieldItem>> entry : tableItem.getRelationTableFields().entrySet()) {
|
||||
for (FieldItem fieldItem : entry.getValue()) {
|
||||
Object value = esTemplate
|
||||
.getValFromRS(mapping, rs, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
// 判断是否是主键
|
||||
if (fieldName.equals(mapping.get_id())) {
|
||||
fieldName = "_id";
|
||||
}
|
||||
paramsTmp.put(fieldName, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.trace(
|
||||
"Join table update es index by query whole sql, destination:{}, table: {}, index: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index());
|
||||
}
|
||||
esTemplate.updateByQuery(config, paramsTmp, esFieldData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表简单字段update
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行data数据
|
||||
* @param old 单行old数据
|
||||
*/
|
||||
private void singleTableSimpleFiledUpdate(ESSyncConfig config, Dml dml, Map<String, Object> data,
|
||||
Map<String, Object> old) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
|
||||
Object idVal = esTemplate.getESDataFromDmlData(mapping, data, old, esFieldData);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Main table update ot es index, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.update(mapping, idVal, esFieldData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主表(单表)复杂字段update
|
||||
*
|
||||
* @param config es配置
|
||||
* @param dml dml信息
|
||||
* @param data 单行dml数据
|
||||
*/
|
||||
private void mainTableUpdate(ESSyncConfig config, Dml dml, Map<String, Object> data, Map<String, Object> old) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
String sql = mapping.getSql();
|
||||
String condition = ESSyncUtil.pkConditionSql(mapping, data);
|
||||
sql = ESSyncUtil.appendCondition(sql, condition);
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Main table update ot es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
sql.replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
try {
|
||||
while (rs.next()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
Object idVal = esTemplate.getESDataFromRS(mapping, rs, old, esFieldData);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(
|
||||
"Main table update ot es index by query sql, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.update(mapping, idVal, esFieldData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交批次
|
||||
*/
|
||||
public void commit() {
|
||||
esTemplate.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Blob;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.joda.time.DateTime;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.ColumnItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.TableItem;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
/**
|
||||
* ES 同步工具同类
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESSyncUtil {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(ESSyncUtil.class);
|
||||
|
||||
public static Object convertToEsObj(Object val, String fieldInfo) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (fieldInfo.startsWith("array:")) {
|
||||
String separator = fieldInfo.substring("array:".length()).trim();
|
||||
String[] values = val.toString().split(separator);
|
||||
return Arrays.asList(values);
|
||||
} else if (fieldInfo.startsWith("object")) {
|
||||
return JSON.parse(val.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型转换为Mapping中对应的类型
|
||||
*/
|
||||
public static Object typeConvert(Object val, String esType) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (esType == null) {
|
||||
return val;
|
||||
}
|
||||
Object res = null;
|
||||
if ("integer".equals(esType)) {
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).intValue();
|
||||
} else {
|
||||
res = Integer.parseInt(val.toString());
|
||||
}
|
||||
} else if ("long".equals(esType)) {
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).longValue();
|
||||
} else {
|
||||
res = Long.parseLong(val.toString());
|
||||
}
|
||||
} else if ("short".equals(esType)) {
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).shortValue();
|
||||
} else {
|
||||
res = Short.parseShort(val.toString());
|
||||
}
|
||||
} else if ("byte".equals(esType)) {
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).byteValue();
|
||||
} else {
|
||||
res = Byte.parseByte(val.toString());
|
||||
}
|
||||
} else if ("double".equals(esType)) {
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).doubleValue();
|
||||
} else {
|
||||
res = Double.parseDouble(val.toString());
|
||||
}
|
||||
} else if ("float".equals(esType) || "half_float".equals(esType) || "scaled_float".equals(esType)) {
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).floatValue();
|
||||
} else {
|
||||
res = Float.parseFloat(val.toString());
|
||||
}
|
||||
} else if ("boolean".equals(esType)) {
|
||||
if (val instanceof Boolean) {
|
||||
res = val;
|
||||
} else if (val instanceof Number) {
|
||||
int v = ((Number) val).intValue();
|
||||
res = v != 0;
|
||||
} else {
|
||||
res = Boolean.parseBoolean(val.toString());
|
||||
}
|
||||
} else if ("date".equals(esType)) {
|
||||
if (val instanceof java.sql.Time) {
|
||||
DateTime dateTime = new DateTime(((java.sql.Time) val).getTime());
|
||||
if (dateTime.getMillisOfSecond() != 0) {
|
||||
res = dateTime.toString("HH:mm:ss.SSS");
|
||||
} else {
|
||||
res = dateTime.toString("HH:mm:ss");
|
||||
}
|
||||
} else if (val instanceof java.sql.Timestamp) {
|
||||
DateTime dateTime = new DateTime(((java.sql.Timestamp) val).getTime());
|
||||
if (dateTime.getMillisOfSecond() != 0) {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS" + Util.timeZone);
|
||||
} else {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss" + Util.timeZone);
|
||||
}
|
||||
} else if (val instanceof java.sql.Date || val instanceof Date) {
|
||||
DateTime dateTime;
|
||||
if (val instanceof java.sql.Date) {
|
||||
dateTime = new DateTime(((java.sql.Date) val).getTime());
|
||||
} else {
|
||||
dateTime = new DateTime(((Date) val).getTime());
|
||||
}
|
||||
if (dateTime.getHourOfDay() == 0 && dateTime.getMinuteOfHour() == 0 && dateTime.getSecondOfMinute() == 0
|
||||
&& dateTime.getMillisOfSecond() == 0) {
|
||||
res = dateTime.toString("yyyy-MM-dd");
|
||||
} else {
|
||||
if (dateTime.getMillisOfSecond() != 0) {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS" + Util.timeZone);
|
||||
} else {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss" + Util.timeZone);
|
||||
}
|
||||
}
|
||||
} else if (val instanceof Long) {
|
||||
DateTime dateTime = new DateTime(((Long) val).longValue());
|
||||
if (dateTime.getHourOfDay() == 0 && dateTime.getMinuteOfHour() == 0 && dateTime.getSecondOfMinute() == 0
|
||||
&& dateTime.getMillisOfSecond() == 0) {
|
||||
res = dateTime.toString("yyyy-MM-dd");
|
||||
} else if (dateTime.getMillisOfSecond() != 0) {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS" + Util.timeZone);
|
||||
} else {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss" + Util.timeZone);
|
||||
}
|
||||
} else if (val instanceof String) {
|
||||
String v = ((String) val).trim();
|
||||
if (v.length() > 18 && v.charAt(4) == '-' && v.charAt(7) == '-' && v.charAt(10) == ' '
|
||||
&& v.charAt(13) == ':' && v.charAt(16) == ':') {
|
||||
String dt = v.substring(0, 10) + "T" + v.substring(11);
|
||||
Date date = Util.parseDate(dt);
|
||||
if (date != null) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
if (dateTime.getMillisOfSecond() != 0) {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS" + Util.timeZone);
|
||||
} else {
|
||||
res = dateTime.toString("yyyy-MM-dd'T'HH:mm:ss" + Util.timeZone);
|
||||
}
|
||||
}
|
||||
} else if (v.length() == 10 && v.charAt(4) == '-' && v.charAt(7) == '-') {
|
||||
Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
res = dateTime.toString("yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("binary".equals(esType)) {
|
||||
if (val instanceof byte[]) {
|
||||
Base64 base64 = new Base64();
|
||||
res = base64.encodeAsString((byte[]) val);
|
||||
} else if (val instanceof Blob) {
|
||||
byte[] b = blobToBytes((Blob) val);
|
||||
Base64 base64 = new Base64();
|
||||
res = base64.encodeAsString(b);
|
||||
} else if (val instanceof String) {
|
||||
// 对应canal中的单字节编码
|
||||
byte[] b = ((String) val).getBytes(StandardCharsets.ISO_8859_1);
|
||||
Base64 base64 = new Base64();
|
||||
res = base64.encodeAsString(b);
|
||||
}
|
||||
} else if ("geo_point".equals(esType)) {
|
||||
if (!(val instanceof String)) {
|
||||
logger.error("es type is geo_point, but source type is not String");
|
||||
return val;
|
||||
}
|
||||
|
||||
if (!((String) val).contains(",")) {
|
||||
logger.error("es type is geo_point, source value not contains ',' separator");
|
||||
return val;
|
||||
}
|
||||
|
||||
String[] point = ((String) val).split(",");
|
||||
Map<String, Double> location = new HashMap<>();
|
||||
location.put("lat", Double.valueOf(point[0].trim()));
|
||||
location.put("lon", Double.valueOf(point[1].trim()));
|
||||
return location;
|
||||
} else if ("array".equals(esType)) {
|
||||
if ("".equals(val.toString().trim())) {
|
||||
res = new ArrayList<>();
|
||||
} else {
|
||||
String value = val.toString();
|
||||
String separator = ",";
|
||||
if (!value.contains(",")) {
|
||||
if (value.contains(";")) {
|
||||
separator = ";";
|
||||
} else if (value.contains("|")) {
|
||||
separator = "|";
|
||||
} else if (value.contains("-")) {
|
||||
separator = "-";
|
||||
}
|
||||
}
|
||||
String[] values = value.split(separator);
|
||||
return Arrays.asList(values);
|
||||
}
|
||||
} else if ("object".equals(esType)) {
|
||||
if ("".equals(val.toString().trim())) {
|
||||
res = new HashMap<>();
|
||||
} else {
|
||||
res = JSON.parseObject(val.toString(), Map.class);
|
||||
}
|
||||
} else {
|
||||
// 其他类全以字符串处理
|
||||
res = val.toString();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blob转byte[]
|
||||
*/
|
||||
private static byte[] blobToBytes(Blob blob) {
|
||||
try (InputStream is = blob.getBinaryStream()) {
|
||||
byte[] b = new byte[(int) blob.length()];
|
||||
if (is.read(b) != -1) {
|
||||
return b;
|
||||
} else {
|
||||
return new byte[0];
|
||||
}
|
||||
} catch (IOException | SQLException e) {
|
||||
logger.error(e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接主键条件
|
||||
*
|
||||
* @param mapping
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static String pkConditionSql(ESMapping mapping, Map<String, Object> data) {
|
||||
Set<ColumnItem> idColumns = new LinkedHashSet<>();
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
|
||||
TableItem mainTable = schemaItem.getMainTable();
|
||||
|
||||
for (ColumnItem idColumnItem : schemaItem.getIdFieldItem(mapping).getColumnItems()) {
|
||||
if ((mainTable.getAlias() == null && idColumnItem.getOwner() == null)
|
||||
|| (mainTable.getAlias() != null && mainTable.getAlias().equals(idColumnItem.getOwner()))) {
|
||||
idColumns.add(idColumnItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (idColumns.isEmpty()) {
|
||||
throw new RuntimeException("Not found primary key field in main table");
|
||||
}
|
||||
|
||||
// 拼接condition
|
||||
StringBuilder condition = new StringBuilder(" ");
|
||||
for (ColumnItem idColumn : idColumns) {
|
||||
Object idVal = data.get(idColumn.getColumnName());
|
||||
if (mainTable.getAlias() != null) condition.append(mainTable.getAlias()).append(".");
|
||||
condition.append(idColumn.getColumnName()).append("=");
|
||||
if (idVal instanceof String) {
|
||||
condition.append("'").append(idVal).append("' AND ");
|
||||
} else {
|
||||
condition.append(idVal).append(" AND ");
|
||||
}
|
||||
}
|
||||
|
||||
if (condition.toString().endsWith("AND ")) {
|
||||
int len2 = condition.length();
|
||||
condition.delete(len2 - 4, len2);
|
||||
}
|
||||
return condition.toString();
|
||||
}
|
||||
|
||||
public static String appendCondition(String sql, String condition) {
|
||||
return sql + " WHERE " + condition + " ";
|
||||
}
|
||||
|
||||
public static void appendCondition(StringBuilder sql, Object value, String owner, String columnName) {
|
||||
if (value instanceof String) {
|
||||
sql.append(owner).append(".").append(columnName).append("='").append(value).append("' AND ");
|
||||
} else {
|
||||
sql.append(owner).append(".").append(columnName).append("=").append(value).append(" AND ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.support;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.action.update.UpdateRequestBuilder;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.cluster.metadata.MappingMetaData;
|
||||
import org.elasticsearch.common.collect.ImmutableOpenMap;
|
||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.ColumnItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
/**
|
||||
* ES 操作模板
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESTemplate {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ESTemplate.class);
|
||||
|
||||
private static final int MAX_BATCH_SIZE = 1000;
|
||||
|
||||
private TransportClient transportClient;
|
||||
|
||||
private BulkRequestBuilder bulkRequestBuilder;
|
||||
|
||||
public ESTemplate(TransportClient transportClient){
|
||||
this.transportClient = transportClient;
|
||||
this.bulkRequestBuilder = transportClient.prepareBulk();
|
||||
}
|
||||
|
||||
public BulkRequestBuilder getBulk() {
|
||||
return bulkRequestBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据
|
||||
*
|
||||
* @param mapping 配置对象
|
||||
* @param pkVal 主键值
|
||||
* @param esFieldData 数据Map
|
||||
*/
|
||||
public void insert(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
UpdateRequestBuilder updateRequestBuilder = transportClient
|
||||
.prepareUpdate(mapping.get_index(), mapping.get_type(), pkVal.toString())
|
||||
.setDoc(esFieldData)
|
||||
.setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequestBuilder.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(updateRequestBuilder);
|
||||
} else {
|
||||
IndexRequestBuilder indexRequestBuilder = transportClient
|
||||
.prepareIndex(mapping.get_index(), mapping.get_type(), pkVal.toString())
|
||||
.setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
indexRequestBuilder.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(indexRequestBuilder);
|
||||
}
|
||||
commitBulk();
|
||||
} else {
|
||||
SearchResponse response = transportClient.prepareSearch(mapping.get_index())
|
||||
.setTypes(mapping.get_type())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.setSize(10000)
|
||||
.get();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
getBulk().add(transportClient.prepareUpdate(mapping.get_index(), mapping.get_type(), hit.getId())
|
||||
.setDoc(esFieldData));
|
||||
commitBulk();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键更新数据
|
||||
*
|
||||
* @param mapping 配置对象
|
||||
* @param pkVal 主键值
|
||||
* @param esFieldData 数据Map
|
||||
*/
|
||||
public void update(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
Map<String, Object> esFieldDataTmp = new LinkedHashMap<>(esFieldData.size());
|
||||
esFieldData.forEach((k, v) -> esFieldDataTmp.put(Util.cleanColumn(k), v));
|
||||
append4Update(mapping, pkVal, esFieldDataTmp);
|
||||
commitBulk();
|
||||
}
|
||||
|
||||
/**
|
||||
* update by query
|
||||
*
|
||||
* @param config 配置对象
|
||||
* @param paramsTmp sql查询条件
|
||||
* @param esFieldData 数据Map
|
||||
*/
|
||||
public void updateByQuery(ESSyncConfig config, Map<String, Object> paramsTmp, Map<String, Object> esFieldData) {
|
||||
if (paramsTmp.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
|
||||
paramsTmp.forEach((fieldName, value) -> queryBuilder.must(QueryBuilders.termsQuery(fieldName, value)));
|
||||
|
||||
// 查询sql批量更新
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM (" + mapping.getSql() + ") _v WHERE ");
|
||||
paramsTmp.forEach(
|
||||
(fieldName, value) -> sql.append("_v.").append(fieldName).append("=").append(value).append(" AND "));
|
||||
int len = sql.length();
|
||||
sql.delete(len - 4, len);
|
||||
Integer syncCount = (Integer) Util.sqlRS(ds, sql.toString(), rs -> {
|
||||
int count = 0;
|
||||
try {
|
||||
while (rs.next()) {
|
||||
Object idVal = getIdValFromRS(mapping, rs);
|
||||
append4Update(mapping, idVal, esFieldData);
|
||||
commitBulk();
|
||||
count++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Update ES by query affected {} records", syncCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过主键删除数据
|
||||
*
|
||||
* @param mapping 配置对象
|
||||
* @param pkVal 主键值
|
||||
* @param esFieldData 数据Map
|
||||
*/
|
||||
public void delete(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
getBulk().add(transportClient.prepareDelete(mapping.get_index(), mapping.get_type(), pkVal.toString()));
|
||||
commitBulk();
|
||||
} else {
|
||||
SearchResponse response = transportClient.prepareSearch(mapping.get_index())
|
||||
.setTypes(mapping.get_type())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.setSize(10000)
|
||||
.get();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
getBulk().add(transportClient.prepareUpdate(mapping.get_index(), mapping.get_type(), hit.getId())
|
||||
.setDoc(esFieldData));
|
||||
commitBulk();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交批次
|
||||
*/
|
||||
public void commit() {
|
||||
if (getBulk().numberOfActions() > 0) {
|
||||
BulkResponse response = getBulk().execute().actionGet();
|
||||
if (response.hasFailures()) {
|
||||
for (BulkItemResponse itemResponse : response.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.error(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
throw new RuntimeException("ES sync commit error" + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果大于批量数则提交批次
|
||||
*/
|
||||
private void commitBulk() {
|
||||
if (getBulk().numberOfActions() >= MAX_BATCH_SIZE) {
|
||||
commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void append4Update(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
UpdateRequestBuilder updateRequestBuilder = transportClient
|
||||
.prepareUpdate(mapping.get_index(), mapping.get_type(), pkVal.toString())
|
||||
.setDoc(esFieldData)
|
||||
.setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequestBuilder.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(updateRequestBuilder);
|
||||
} else {
|
||||
UpdateRequestBuilder updateRequestBuilder = transportClient
|
||||
.prepareUpdate(mapping.get_index(), mapping.get_type(), pkVal.toString())
|
||||
.setDoc(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequestBuilder.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(updateRequestBuilder);
|
||||
}
|
||||
} else {
|
||||
SearchResponse response = transportClient.prepareSearch(mapping.get_index())
|
||||
.setTypes(mapping.get_type())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.setSize(10000)
|
||||
.get();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
getBulk().add(transportClient.prepareUpdate(mapping.get_index(), mapping.get_type(), hit.getId())
|
||||
.setDoc(esFieldData));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Object getValFromRS(ESMapping mapping, ResultSet resultSet, String fieldName,
|
||||
String columnName) throws SQLException {
|
||||
fieldName = Util.cleanColumn(fieldName);
|
||||
columnName = Util.cleanColumn(columnName);
|
||||
String esType = getEsType(mapping, fieldName);
|
||||
|
||||
Object value = resultSet.getObject(columnName);
|
||||
if (value instanceof Boolean) {
|
||||
if (!"boolean".equals(esType)) {
|
||||
value = resultSet.getByte(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是对象类型
|
||||
if (mapping.getObjFields().containsKey(fieldName)) {
|
||||
return ESSyncUtil.convertToEsObj(value, mapping.getObjFields().get(fieldName));
|
||||
} else {
|
||||
return ESSyncUtil.typeConvert(value, esType);
|
||||
}
|
||||
}
|
||||
|
||||
public Object getESDataFromRS(ESMapping mapping, ResultSet resultSet,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.get_id())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationDataFromRS(mapping, schemaItem, resultSet, esFieldData);
|
||||
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
public Object getIdValFromRS(ESMapping mapping, ResultSet resultSet) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
public Object getESDataFromRS(ESMapping mapping, ResultSet resultSet, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
}
|
||||
|
||||
for (ColumnItem columnItem : fieldItem.getColumnItems()) {
|
||||
if (dmlOld.containsKey(columnItem.getColumnName())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
|
||||
getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationDataFromRS(mapping, schemaItem, resultSet, esFieldData);
|
||||
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
public Object getValFromData(ESMapping mapping, Map<String, Object> dmlData, String fieldName, String columnName) {
|
||||
String esType = getEsType(mapping, fieldName);
|
||||
Object value = dmlData.get(columnName);
|
||||
if (value instanceof Byte) {
|
||||
if ("boolean".equals(esType)) {
|
||||
value = ((Byte) value).intValue() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是对象类型
|
||||
if (mapping.getObjFields().containsKey(fieldName)) {
|
||||
return ESSyncUtil.convertToEsObj(value, mapping.getObjFields().get(fieldName));
|
||||
} else {
|
||||
return ESSyncUtil.typeConvert(value, esType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将dml的data转换为es的data
|
||||
*
|
||||
* @param mapping 配置mapping
|
||||
* @param dmlData dml data
|
||||
* @param esFieldData es data
|
||||
* @return 返回 id 值
|
||||
*/
|
||||
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData,
|
||||
Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
String columnName = fieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
Object value = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.get_id())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationData(mapping, schemaItem, dmlData, esFieldData);
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将dml的data, old转换为es的data
|
||||
*
|
||||
* @param mapping 配置mapping
|
||||
* @param dmlData dml data
|
||||
* @param esFieldData es data
|
||||
* @return 返回 id 值
|
||||
*/
|
||||
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
String columnName = fieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||
}
|
||||
|
||||
if (dmlOld.containsKey(columnName) && !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
|
||||
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationData(mapping, schemaItem, dmlOld, esFieldData);
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
private void putRelationDataFromRS(ESMapping mapping, SchemaItem schemaItem, ResultSet resultSet,
|
||||
Map<String, Object> esFieldData) {
|
||||
// 添加父子文档关联信息
|
||||
if (!mapping.getRelations().isEmpty()) {
|
||||
mapping.getRelations().forEach((relationField, relationMapping) -> {
|
||||
Map<String, Object> relations = new HashMap<>();
|
||||
relations.put("name", relationMapping.getName());
|
||||
if (StringUtils.isNotEmpty(relationMapping.getParent())) {
|
||||
FieldItem parentFieldItem = schemaItem.getSelectFields().get(relationMapping.getParent());
|
||||
Object parentVal;
|
||||
try {
|
||||
parentVal = getValFromRS(mapping,
|
||||
resultSet,
|
||||
parentFieldItem.getFieldName(),
|
||||
parentFieldItem.getFieldName());
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (parentVal != null) {
|
||||
relations.put("parent", parentVal.toString());
|
||||
esFieldData.put("$parent_routing", parentVal.toString());
|
||||
|
||||
}
|
||||
}
|
||||
esFieldData.put(relationField, relations);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void putRelationData(ESMapping mapping, SchemaItem schemaItem, Map<String, Object> dmlData,
|
||||
Map<String, Object> esFieldData) {
|
||||
// 添加父子文档关联信息
|
||||
if (!mapping.getRelations().isEmpty()) {
|
||||
mapping.getRelations().forEach((relationField, relationMapping) -> {
|
||||
Map<String, Object> relations = new HashMap<>();
|
||||
relations.put("name", relationMapping.getName());
|
||||
if (StringUtils.isNotEmpty(relationMapping.getParent())) {
|
||||
FieldItem parentFieldItem = schemaItem.getSelectFields().get(relationMapping.getParent());
|
||||
String columnName = parentFieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
Object parentVal = getValFromData(mapping, dmlData, parentFieldItem.getFieldName(), columnName);
|
||||
if (parentVal != null) {
|
||||
relations.put("parent", parentVal.toString());
|
||||
esFieldData.put("$parent_routing", parentVal.toString());
|
||||
|
||||
}
|
||||
}
|
||||
esFieldData.put(relationField, relations);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* es 字段类型本地缓存
|
||||
*/
|
||||
private static ConcurrentMap<String, Map<String, String>> esFieldTypes = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取es mapping中的属性类型
|
||||
*
|
||||
* @param mapping mapping配置
|
||||
* @param fieldName 属性名
|
||||
* @return 类型
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String getEsType(ESMapping mapping, String fieldName) {
|
||||
String key = mapping.get_index() + "-" + mapping.get_type();
|
||||
Map<String, String> fieldType = esFieldTypes.get(key);
|
||||
if (fieldType == null) {
|
||||
ImmutableOpenMap<String, MappingMetaData> mappings;
|
||||
try {
|
||||
mappings = transportClient.admin()
|
||||
.cluster()
|
||||
.prepareState()
|
||||
.execute()
|
||||
.actionGet()
|
||||
.getState()
|
||||
.getMetaData()
|
||||
.getIndices()
|
||||
.get(mapping.get_index())
|
||||
.getMappings();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.get_index());
|
||||
}
|
||||
MappingMetaData mappingMetaData = mappings.get(mapping.get_type());
|
||||
if (mappingMetaData == null) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.get_index());
|
||||
}
|
||||
|
||||
fieldType = new LinkedHashMap<>();
|
||||
|
||||
Map<String, Object> sourceMap = mappingMetaData.getSourceAsMap();
|
||||
Map<String, Object> esMapping = (Map<String, Object>) sourceMap.get("properties");
|
||||
for (Map.Entry<String, Object> entry : esMapping.entrySet()) {
|
||||
Map<String, Object> value = (Map<String, Object>) entry.getValue();
|
||||
if (value.containsKey("properties")) {
|
||||
fieldType.put(entry.getKey(), "object");
|
||||
} else {
|
||||
fieldType.put(entry.getKey(), (String) value.get("type"));
|
||||
}
|
||||
}
|
||||
esFieldTypes.put(key, fieldType);
|
||||
}
|
||||
|
||||
return fieldType.get(fieldName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
es=com.alibaba.otter.canal.client.adapter.es.ESAdapter
|
||||
@@ -0,0 +1,21 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
esMapping:
|
||||
_index: customer
|
||||
_type: _doc
|
||||
_id: _id
|
||||
relations:
|
||||
customer_order:
|
||||
name: order
|
||||
parent: customer_id
|
||||
sql: "select concat('oid_', t.id) as _id,
|
||||
t.customer_id,
|
||||
t.id as order_id,
|
||||
t.serial_code as order_serial,
|
||||
t.c_time as order_time
|
||||
from biz_order t"
|
||||
skips:
|
||||
- customer_id
|
||||
etlCondition: "where t.c_time>='{0}'"
|
||||
commitBatch: 3000
|
||||
@@ -0,0 +1,47 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
esMapping:
|
||||
_index: customer
|
||||
_type: _doc
|
||||
_id: id
|
||||
relations:
|
||||
customer_order:
|
||||
name: customer
|
||||
sql: "select t.id, t.name, t.email from customer t"
|
||||
etlCondition: "where t.c_time>='{0}'"
|
||||
commitBatch: 3000
|
||||
|
||||
|
||||
#{
|
||||
# "mappings":{
|
||||
# "_doc":{
|
||||
# "properties":{
|
||||
# "id": {
|
||||
# "type": "long"
|
||||
# },
|
||||
# "name": {
|
||||
# "type": "text"
|
||||
# },
|
||||
# "email": {
|
||||
# "type": "text"
|
||||
# },
|
||||
# "order_id": {
|
||||
# "type": "long"
|
||||
# },
|
||||
# "order_serial": {
|
||||
# "type": "text"
|
||||
# },
|
||||
# "order_time": {
|
||||
# "type": "date"
|
||||
# },
|
||||
# "customer_order":{
|
||||
# "type":"join",
|
||||
# "relations":{
|
||||
# "customer":"order"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
@@ -0,0 +1,16 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
esMapping:
|
||||
_index: mytest_user
|
||||
_type: _doc
|
||||
_id: _id
|
||||
upsert: true
|
||||
# pk: id
|
||||
sql: "select a.id as _id, a.name as _name, a.role_id as _role_id, b.role_name as _role_name,
|
||||
a.c_time as _c_time from user a
|
||||
left join role b on b.id=a.role_id"
|
||||
# objFields:
|
||||
# _labels: array:;
|
||||
etlCondition: "where a.c_time>='{0}'"
|
||||
commitBatch: 3000
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfigLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
|
||||
public class ConfigLoadTest {
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
// AdapterConfigs.put("es", "mytest_user.yml");
|
||||
// 加载数据源连接池
|
||||
DatasourceConfig.DATA_SOURCES.put("defaultDS", TestConstant.dataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoad() {
|
||||
Map<String, ESSyncConfig> configMap = ESSyncConfigLoader.load(null);
|
||||
ESSyncConfig config = configMap.get("mytest_user.yml");
|
||||
config.validate();
|
||||
Assert.assertNotNull(config);
|
||||
Assert.assertEquals("defaultDS", config.getDataSourceKey());
|
||||
ESSyncConfig.ESMapping esMapping = config.getEsMapping();
|
||||
Assert.assertEquals("mytest_user", esMapping.get_index());
|
||||
Assert.assertEquals("_doc", esMapping.get_type());
|
||||
Assert.assertEquals("id", esMapping.get_id());
|
||||
Assert.assertNotNull(esMapping.getSql());
|
||||
|
||||
// Map<String, List<ESSyncConfig>> dbTableEsSyncConfig =
|
||||
// ESSyncConfigLoader.getDbTableEsSyncConfig();
|
||||
// Assert.assertFalse(dbTableEsSyncConfig.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.elasticsearch.transport.client.PreBuiltTransportClient;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ESTest {
|
||||
|
||||
private TransportClient transportClient;
|
||||
|
||||
@Before
|
||||
public void init() throws UnknownHostException {
|
||||
Settings.Builder settingBuilder = Settings.builder();
|
||||
settingBuilder.put("cluster.name", TestConstant.clusterName);
|
||||
Settings settings = settingBuilder.build();
|
||||
transportClient = new PreBuiltTransportClient(settings);
|
||||
String[] hostArray = TestConstant.esHosts.split(",");
|
||||
for (String host : hostArray) {
|
||||
int i = host.indexOf(":");
|
||||
transportClient.addTransportAddress(new TransportAddress(InetAddress.getByName(host.substring(0, i)),
|
||||
Integer.parseInt(host.substring(i + 1))));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test01() {
|
||||
SearchResponse response = transportClient.prepareSearch("test")
|
||||
.setTypes("osm")
|
||||
.setQuery(QueryBuilders.termQuery("_id", "1"))
|
||||
.setSize(10000)
|
||||
.get();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
System.out.println(hit.getSourceAsMap().get("data").getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test02() {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
esFieldData.put("userId", 2L);
|
||||
esFieldData.put("eventId", 4L);
|
||||
esFieldData.put("eventName", "网络异常");
|
||||
esFieldData.put("description", "第四个事件信息");
|
||||
|
||||
Map<String, Object> relations = new LinkedHashMap<>();
|
||||
esFieldData.put("user_event", relations);
|
||||
relations.put("name", "event");
|
||||
relations.put("parent", "2");
|
||||
|
||||
BulkRequestBuilder bulkRequestBuilder = transportClient.prepareBulk();
|
||||
bulkRequestBuilder
|
||||
.add(transportClient.prepareIndex("test", "osm", "2_4").setRouting("2").setSource(esFieldData));
|
||||
commit(bulkRequestBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test03() {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
esFieldData.put("userId", 2L);
|
||||
esFieldData.put("eventName", "网络异常1");
|
||||
|
||||
Map<String, Object> relations = new LinkedHashMap<>();
|
||||
esFieldData.put("user_event", relations);
|
||||
relations.put("name", "event");
|
||||
relations.put("parent", "2");
|
||||
|
||||
BulkRequestBuilder bulkRequestBuilder = transportClient.prepareBulk();
|
||||
bulkRequestBuilder.add(transportClient.prepareUpdate("test", "osm", "2_4").setRouting("2").setDoc(esFieldData));
|
||||
commit(bulkRequestBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test04() {
|
||||
BulkRequestBuilder bulkRequestBuilder = transportClient.prepareBulk();
|
||||
bulkRequestBuilder.add(transportClient.prepareDelete("test", "osm", "2_4"));
|
||||
commit(bulkRequestBuilder);
|
||||
}
|
||||
|
||||
private void commit(BulkRequestBuilder bulkRequestBuilder) {
|
||||
if (bulkRequestBuilder.numberOfActions() > 0) {
|
||||
BulkResponse response = bulkRequestBuilder.execute().actionGet();
|
||||
if (response.hasFailures()) {
|
||||
for (BulkItemResponse itemResponse : response.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
System.out.println(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
System.out.println("ES bulk commit error" + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
transportClient.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SchemaItem.TableItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.SqlParser;
|
||||
|
||||
public class SqlParseTest {
|
||||
|
||||
@Test
|
||||
public void parseTest() {
|
||||
String sql = "select a.id, CASE WHEN a.id <= 500 THEN '1' else '2' end as id2, "
|
||||
+ "concat(a.name,'_test') as name, a.role_id, b.name as role_name, c.labels from user a "
|
||||
+ "left join role b on a.role_id=b.id "
|
||||
+ "left join (select user_id, group_concat(label,',') as labels from user_label "
|
||||
+ "group by user_id) c on c.user_id=a.id";
|
||||
SchemaItem schemaItem = SqlParser.parse(sql);
|
||||
|
||||
// 通过表名找 TableItem
|
||||
List<TableItem> tableItems = schemaItem.getTableItemAliases().get("user_label".toLowerCase());
|
||||
tableItems.forEach(tableItem -> Assert.assertEquals("c", tableItem.getAlias()));
|
||||
|
||||
TableItem tableItem = tableItems.get(0);
|
||||
Assert.assertFalse(tableItem.isMain());
|
||||
Assert.assertTrue(tableItem.isSubQuery());
|
||||
// 通过字段名找 FieldItem
|
||||
List<FieldItem> fieldItems = schemaItem.getColumnFields().get(tableItem.getAlias() + ".labels".toLowerCase());
|
||||
fieldItems.forEach(
|
||||
fieldItem -> Assert.assertEquals("c.labels", fieldItem.getOwner() + "." + fieldItem.getFieldName()));
|
||||
|
||||
// 获取当前表关联条件字段
|
||||
Map<FieldItem, List<FieldItem>> relationTableFields = tableItem.getRelationTableFields();
|
||||
relationTableFields.keySet()
|
||||
.forEach(fieldItem -> Assert.assertEquals("user_id", fieldItem.getColumn().getColumnName()));
|
||||
|
||||
// 获取关联字段在select中的对应字段
|
||||
// List<FieldItem> relationSelectFieldItem =
|
||||
// tableItem.getRelationKeyFieldItems();
|
||||
// relationSelectFieldItem.forEach(fieldItem -> Assert.assertEquals("c.labels",
|
||||
// fieldItem.getOwner() + "." + fieldItem.getColumn().getColumnName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
|
||||
public class TestConstant {
|
||||
|
||||
public final static String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true";
|
||||
public final static String jdbcUser = "root";
|
||||
public final static String jdbcPassword = "121212";
|
||||
|
||||
public final static String esHosts = "127.0.0.1:9300";
|
||||
public final static String clusterName = "elasticsearch";
|
||||
|
||||
public final static DruidDataSource dataSource;
|
||||
|
||||
static {
|
||||
dataSource = new DruidDataSource();
|
||||
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
|
||||
dataSource.setUrl(jdbcUrl);
|
||||
dataSource.setUsername(jdbcUser);
|
||||
dataSource.setPassword(jdbcPassword);
|
||||
dataSource.setInitialSize(1);
|
||||
dataSource.setMinIdle(1);
|
||||
dataSource.setMaxActive(1);
|
||||
dataSource.setMaxWait(60000);
|
||||
dataSource.setTimeBetweenEvictionRunsMillis(60000);
|
||||
dataSource.setMinEvictableIdleTimeMillis(300000);
|
||||
dataSource.setPoolPreparedStatements(false);
|
||||
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
|
||||
dataSource.setValidationQuery("select 1");
|
||||
try {
|
||||
dataSource.init();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.test.TestConstant;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.OuterAdapterConfig;
|
||||
|
||||
public class Common {
|
||||
|
||||
public static ESAdapter init() {
|
||||
DatasourceConfig.DATA_SOURCES.put("defaultDS", TestConstant.dataSource);
|
||||
|
||||
OuterAdapterConfig outerAdapterConfig = new OuterAdapterConfig();
|
||||
outerAdapterConfig.setName("es");
|
||||
outerAdapterConfig.setHosts(TestConstant.esHosts);
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put("cluster.name", TestConstant.clusterName);
|
||||
outerAdapterConfig.setProperties(properties);
|
||||
|
||||
ESAdapter esAdapter = new ESAdapter();
|
||||
esAdapter.init(outerAdapterConfig, null);
|
||||
return esAdapter;
|
||||
}
|
||||
|
||||
public static void sqlExe(DataSource dataSource, String sql) {
|
||||
Connection conn = null;
|
||||
Statement stmt = null;
|
||||
try {
|
||||
conn = dataSource.getConnection();
|
||||
conn.setAutoCommit(false);
|
||||
stmt = conn.createStatement();
|
||||
stmt.execute(sql);
|
||||
conn.commit();
|
||||
} catch (Exception e) {
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.rollback();
|
||||
} catch (SQLException e1) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (stmt != null) {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.close();
|
||||
} catch (SQLException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class LabelSyncJoinSub2Test {
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// AdapterConfigs.put("es", "mytest_user_join_sub2.yml");
|
||||
esAdapter = Common.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 带函数子查询从表插入
|
||||
*/
|
||||
@Test
|
||||
public void test01() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from label where id=1 or id=2");
|
||||
Common.sqlExe(ds, "insert into label (id,user_id,label) values (1,1,'a')");
|
||||
Common.sqlExe(ds, "insert into label (id,user_id,label) values (2,1,'b')");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("label");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 2L);
|
||||
data.put("user_id", 1L);
|
||||
data.put("label", "b");
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("b;a_", response.getSource().get("_labels"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 带函数子查询从表更新
|
||||
*/
|
||||
@Test
|
||||
public void test02() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "update label set label='aa' where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("label");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("user_id", 1L);
|
||||
data.put("label", "aa");
|
||||
dml.setData(dataList);
|
||||
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("label", "v");
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("b;aa_", response.getSource().get("_labels"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 带函数子查询从表删除
|
||||
*/
|
||||
@Test
|
||||
public void test03() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from label where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("DELETE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("label");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("user_id", 1L);
|
||||
data.put("label", "a");
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("b_", response.getSource().get("_labels"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class LabelSyncJoinSubTest {
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// AdapterConfigs.put("es", "mytest_user_join_sub.yml");
|
||||
esAdapter = Common.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 子查询从表插入
|
||||
*/
|
||||
@Test
|
||||
public void test01() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from label where id=1 or id=2");
|
||||
Common.sqlExe(ds, "insert into label (id,user_id,label) values (1,1,'a')");
|
||||
Common.sqlExe(ds, "insert into label (id,user_id,label) values (2,1,'b')");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("label");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 2L);
|
||||
data.put("user_id", 1L);
|
||||
data.put("label", "b");
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("b;a", response.getSource().get("_labels"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 子查询从表更新
|
||||
*/
|
||||
@Test
|
||||
public void test02() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "update label set label='aa' where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("label");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("user_id", 1L);
|
||||
data.put("label", "aa");
|
||||
dml.setData(dataList);
|
||||
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("label", "a");
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("b;aa", response.getSource().get("_labels"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 子查询从表删除
|
||||
*/
|
||||
@Test
|
||||
public void test03() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from label where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("DELETE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("label");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("user_id", 1L);
|
||||
data.put("label", "a");
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("b", response.getSource().get("_labels"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class RoleSyncJoinOne2Test {
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// AdapterConfigs.put("es", "mytest_user_join_one2.yml");
|
||||
esAdapter = Common.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 带函数非子查询从表插入
|
||||
*/
|
||||
@Test
|
||||
public void test01() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from role where id=1");
|
||||
Common.sqlExe(ds, "insert into role (id,role_name) values (1,'admin')");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("role");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("role_name", "admin");
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("admin_", response.getSource().get("_role_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 带函数非子查询从表更新
|
||||
*/
|
||||
@Test
|
||||
public void test02() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "update role set role_name='admin3' where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("role");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("role_name", "admin3");
|
||||
dml.setData(dataList);
|
||||
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("role_name", "admin");
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("admin3_", response.getSource().get("_role_name"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class RoleSyncJoinOneTest {
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// AdapterConfigs.put("es", "mytest_user_join_one.yml");
|
||||
esAdapter = Common.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 非子查询从表插入
|
||||
*/
|
||||
@Test
|
||||
public void test01() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from role where id=1");
|
||||
Common.sqlExe(ds, "insert into role (id,role_name) values (1,'admin')");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("role");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("role_name", "admin");
|
||||
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("admin", response.getSource().get("_role_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 非子查询从表更新
|
||||
*/
|
||||
@Test
|
||||
public void test02() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "update role set role_name='admin2' where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("role");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("role_name", "admin2");
|
||||
dml.setData(dataList);
|
||||
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("role_name", "admin");
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("admin2", response.getSource().get("_role_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 主表更新外键值
|
||||
*/
|
||||
@Test
|
||||
public void test03() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from role where id=2");
|
||||
Common.sqlExe(ds, "insert into role (id,role_name) values (2,'operator')");
|
||||
Common.sqlExe(ds, "update user set role_id=2 where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("role_id", 2L);
|
||||
dml.setData(dataList);
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("role_id", 1L);
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("operator", response.getSource().get("_role_name"));
|
||||
|
||||
Common.sqlExe(ds, "update user set role_id=1 where id=1");
|
||||
|
||||
Dml dml2 = new Dml();
|
||||
dml2.setDestination("example");
|
||||
dml2.setTs(new Date().getTime());
|
||||
dml2.setType("UPDATE");
|
||||
dml2.setDatabase("mytest");
|
||||
dml2.setTable("user");
|
||||
List<Map<String, Object>> dataList2 = new ArrayList<>();
|
||||
Map<String, Object> data2 = new LinkedHashMap<>();
|
||||
dataList2.add(data2);
|
||||
data2.put("id", 1L);
|
||||
data2.put("role_id", 1L);
|
||||
dml2.setData(dataList2);
|
||||
List<Map<String, Object>> oldList2 = new ArrayList<>();
|
||||
Map<String, Object> old2 = new LinkedHashMap<>();
|
||||
oldList2.add(old2);
|
||||
old2.put("role_id", 2L);
|
||||
dml2.setOld(oldList2);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml2);
|
||||
|
||||
GetResponse response2 = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("admin2", response2.getSource().get("_role_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 非子查询从表删除
|
||||
*/
|
||||
@Test
|
||||
public void test04() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from role where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("DELETE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("role");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("role_name", "admin");
|
||||
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertNull(response.getSource().get("_role_name"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class UserSyncJoinOneTest {
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// AdapterConfigs.put("es", "mytest_user_join_one.yml");
|
||||
esAdapter = Common.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主表带函数插入
|
||||
*/
|
||||
@Test
|
||||
public void test01() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "delete from user where id=1");
|
||||
Common.sqlExe(ds, "insert into user (id,name,role_id) values (1,'Eric',1)");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric");
|
||||
data.put("role_id", 1L);
|
||||
data.put("c_time", new Date());
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("Eric_", response.getSource().get("_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 主表带函数更新
|
||||
*/
|
||||
@Test
|
||||
public void test02() {
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get("defaultDS");
|
||||
Common.sqlExe(ds, "update user set name='Eric2' where id=1");
|
||||
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric2");
|
||||
dml.setData(dataList);
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("name", "Eric");
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("Eric2_", response.getSource().get("_name"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.test.sync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.elasticsearch.action.get.GetResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class UserSyncSingleTest {
|
||||
|
||||
private ESAdapter esAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
// AdapterConfigs.put("es", "mytest_user_single.yml");
|
||||
esAdapter = Common.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表插入
|
||||
*/
|
||||
@Test
|
||||
public void test01() {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric");
|
||||
data.put("role_id", 1L);
|
||||
data.put("c_time", new Date());
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("Eric", response.getSource().get("_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表更新
|
||||
*/
|
||||
@Test
|
||||
public void test02() {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric2");
|
||||
dml.setData(dataList);
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("name", "Eric");
|
||||
dml.setOld(oldList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertEquals("Eric2", response.getSource().get("_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表删除
|
||||
*/
|
||||
@Test
|
||||
public void test03() {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("DELETE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric");
|
||||
data.put("role_id", 1L);
|
||||
data.put("c_time", new Date());
|
||||
dml.setData(dataList);
|
||||
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, ESSyncConfig> esSyncConfigs = esAdapter.getDbTableEsSyncConfig().get(database + "-" + table);
|
||||
|
||||
esAdapter.getEsSyncService().sync(esSyncConfigs.values(), dml);
|
||||
|
||||
GetResponse response = esAdapter.getTransportClient().prepareGet("mytest_user", "_doc", "1").get();
|
||||
Assert.assertNull(response.getSource());
|
||||
}
|
||||
|
||||
// @After
|
||||
// public void after() {
|
||||
// esAdapter.destroy();
|
||||
// DatasourceConfig.DATA_SOURCES.values().forEach(DruidDataSource::close);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
-- ----------------------------
|
||||
-- Table structure for label
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `label`;
|
||||
CREATE TABLE `label` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`label` varchar(30) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for role
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `role`;
|
||||
CREATE TABLE `role` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`role_name` varchar(30) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for user
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
CREATE TABLE `user` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(30) NOT NULL,
|
||||
`c_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`role_id` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
insert into user (id,name,role_id) values (1,'Eric',1);
|
||||
insert into role (id,role_name) values (1,'admin');
|
||||
insert into role (id,role_name) values (2,'operator');
|
||||
insert into label (id,user_id,label) values (1,1,'a');
|
||||
insert into label (id,user_id,label) values (2,1,'b');
|
||||
commit;
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"_doc": {
|
||||
"properties": {
|
||||
"_name": {
|
||||
"type": "text"
|
||||
},
|
||||
"_role_id": {
|
||||
"type": "long"
|
||||
},
|
||||
"_role_name": {
|
||||
"type": "text"
|
||||
},
|
||||
"_labels": {
|
||||
"type": "text"
|
||||
},
|
||||
"_c_time": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
esMapping:
|
||||
_index: mytest_user
|
||||
_type: _doc
|
||||
_id: _id
|
||||
sql: "select a.id as _id, a.name as _name, a.role_id as _role_id, a.c_time as _c_time from user a"
|
||||
commitBatch: 3000
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="ERROR">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -0,0 +1,13 @@
|
||||
<configuration scan="true" scanPeriod=" 5 seconds">
|
||||
<jmxConfigurator />
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56} - %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="TRACE">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.1</version>
|
||||
<version>1.1.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
@@ -17,12 +17,6 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.19</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hbase</groupId>
|
||||
<artifactId>hbase-client</artifactId>
|
||||
@@ -62,6 +56,26 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<copy todir="${project.basedir}/../launcher/target/classes/hbase" overwrite="true">
|
||||
<fileset dir="${project.basedir}/target/classes/hbase" erroronmissingdir="true">
|
||||
<include name="*.yml" />
|
||||
</fileset>
|
||||
</copy>
|
||||
</tasks>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.alibaba.otter.canal.client.adapter.hbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@@ -12,8 +10,10 @@ import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.hadoop.conf.Configuration;
|
||||
import org.apache.hadoop.hbase.HBaseConfiguration;
|
||||
import org.apache.hadoop.hbase.TableName;
|
||||
import org.apache.hadoop.hbase.client.*;
|
||||
import org.apache.hadoop.hbase.client.HTable;
|
||||
import org.apache.hadoop.hbase.client.Result;
|
||||
import org.apache.hadoop.hbase.client.ResultScanner;
|
||||
import org.apache.hadoop.hbase.client.Scan;
|
||||
import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
|
||||
import com.alibaba.otter.canal.client.adapter.OuterAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfigLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.monitor.HbaseConfigMonitor;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.service.HbaseEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.service.HbaseSyncService;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.HbaseTemplate;
|
||||
@@ -35,55 +36,111 @@ import com.alibaba.otter.canal.client.adapter.support.*;
|
||||
@SPI("hbase")
|
||||
public class HbaseAdapter implements OuterAdapter {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(HbaseAdapter.class);
|
||||
private static Logger logger = LoggerFactory.getLogger(HbaseAdapter.class);
|
||||
|
||||
private static volatile Map<String, MappingConfig> hbaseMapping = null; // 文件名对应配置
|
||||
private static volatile Map<String, MappingConfig> mappingConfigCache = null; // 库名-表名对应配置
|
||||
private Map<String, MappingConfig> hbaseMapping = new ConcurrentHashMap<>(); // 文件名对应配置
|
||||
private Map<String, Map<String, MappingConfig>> mappingConfigCache = new ConcurrentHashMap<>(); // 库名-表名对应配置
|
||||
|
||||
private Connection conn;
|
||||
private HbaseSyncService hbaseSyncService;
|
||||
private HbaseTemplate hbaseTemplate;
|
||||
private HbaseSyncService hbaseSyncService;
|
||||
private HbaseTemplate hbaseTemplate;
|
||||
|
||||
private HbaseConfigMonitor configMonitor;
|
||||
|
||||
private Properties envProperties;
|
||||
|
||||
public Map<String, MappingConfig> getHbaseMapping() {
|
||||
return hbaseMapping;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, MappingConfig>> getMappingConfigCache() {
|
||||
return mappingConfigCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(OuterAdapterConfig configuration) {
|
||||
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||
try {
|
||||
if (mappingConfigCache == null) {
|
||||
synchronized (MappingConfig.class) {
|
||||
if (mappingConfigCache == null) {
|
||||
hbaseMapping = MappingConfigLoader.load();
|
||||
mappingConfigCache = new HashMap<>();
|
||||
for (MappingConfig mappingConfig : hbaseMapping.values()) {
|
||||
mappingConfigCache.put(StringUtils.trimToEmpty(mappingConfig.getHbaseMapping().getDestination())
|
||||
+ "." + mappingConfig.getHbaseMapping().getDatabase() + "."
|
||||
+ mappingConfig.getHbaseMapping().getTable(),
|
||||
mappingConfig);
|
||||
}
|
||||
}
|
||||
this.envProperties = envProperties;
|
||||
Map<String, MappingConfig> hbaseMappingTmp = MappingConfigLoader.load(envProperties);
|
||||
// 过滤不匹配的key的配置
|
||||
hbaseMappingTmp.forEach((key, mappingConfig) -> {
|
||||
if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
|
||||
|| (mappingConfig.getOuterAdapterKey() != null
|
||||
&& mappingConfig.getOuterAdapterKey().equalsIgnoreCase(configuration.getKey()))) {
|
||||
hbaseMapping.put(key, mappingConfig);
|
||||
}
|
||||
});
|
||||
for (Map.Entry<String, MappingConfig> entry : hbaseMapping.entrySet()) {
|
||||
String configName = entry.getKey();
|
||||
MappingConfig mappingConfig = entry.getValue();
|
||||
String k;
|
||||
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||
k = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "-"
|
||||
+ StringUtils.trimToEmpty(mappingConfig.getGroupId()) + "_"
|
||||
+ mappingConfig.getHbaseMapping().getDatabase() + "-"
|
||||
+ mappingConfig.getHbaseMapping().getTable();
|
||||
} else {
|
||||
k = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
|
||||
+ mappingConfig.getHbaseMapping().getDatabase() + "-"
|
||||
+ mappingConfig.getHbaseMapping().getTable();
|
||||
}
|
||||
Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(k,
|
||||
k1 -> new ConcurrentHashMap<>());
|
||||
configMap.put(configName, mappingConfig);
|
||||
}
|
||||
|
||||
Map<String, String> propertites = configuration.getProperties();
|
||||
Map<String, String> properties = configuration.getProperties();
|
||||
|
||||
Configuration hbaseConfig = HBaseConfiguration.create();
|
||||
propertites.forEach(hbaseConfig::set);
|
||||
conn = ConnectionFactory.createConnection(hbaseConfig);
|
||||
hbaseTemplate = new HbaseTemplate(conn);
|
||||
properties.forEach(hbaseConfig::set);
|
||||
hbaseTemplate = new HbaseTemplate(hbaseConfig);
|
||||
hbaseSyncService = new HbaseSyncService(hbaseTemplate);
|
||||
|
||||
configMonitor = new HbaseConfigMonitor();
|
||||
configMonitor.init(this, envProperties);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sync(Dml dml) {
|
||||
public void sync(List<Dml> dmls) {
|
||||
if (dmls == null || dmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Dml dml : dmls) {
|
||||
sync(dml);
|
||||
}
|
||||
}
|
||||
|
||||
private void sync(Dml dml) {
|
||||
if (dml == null) {
|
||||
return;
|
||||
}
|
||||
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
||||
String groupId = StringUtils.trimToEmpty(dml.getGroupId());
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
MappingConfig config = mappingConfigCache.get(destination + "." + database + "." + table);
|
||||
hbaseSyncService.sync(config, dml);
|
||||
Map<String, MappingConfig> configMap;
|
||||
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||
configMap = mappingConfigCache.get(destination + "-" + groupId + "_" + database + "-" + table);
|
||||
} else {
|
||||
configMap = mappingConfigCache.get(destination + "_" + database + "-" + table);
|
||||
}
|
||||
if (configMap != null) {
|
||||
List<MappingConfig> configs = new ArrayList<>();
|
||||
configMap.values().forEach(config -> {
|
||||
if (StringUtils.isNotEmpty(config.getGroupId())) {
|
||||
if (config.getGroupId().equals(dml.getGroupId())) {
|
||||
configs.add(config);
|
||||
}
|
||||
} else {
|
||||
configs.add(config);
|
||||
}
|
||||
});
|
||||
if (!configs.isEmpty()) {
|
||||
configs.forEach(config -> hbaseSyncService.sync(config, dml));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,33 +157,34 @@ public class HbaseAdapter implements OuterAdapter {
|
||||
return etlResult;
|
||||
}
|
||||
} else {
|
||||
DataSource dataSource = DatasourceConfig.DATA_SOURCES.get(task);
|
||||
if (dataSource != null) {
|
||||
StringBuilder resultMsg = new StringBuilder();
|
||||
boolean resSucc = true;
|
||||
// ds不为空说明传入的是datasourceKey
|
||||
for (MappingConfig configTmp : hbaseMapping.values()) {
|
||||
// 取所有的datasourceKey为task的配置
|
||||
if (configTmp.getDataSourceKey().equals(task)) {
|
||||
EtlResult etlRes = HbaseEtlService.importData(dataSource, hbaseTemplate, configTmp, params);
|
||||
if (!etlRes.getSucceeded()) {
|
||||
resSucc = false;
|
||||
resultMsg.append(etlRes.getErrorMessage()).append("\n");
|
||||
} else {
|
||||
resultMsg.append(etlRes.getResultMessage()).append("\n");
|
||||
}
|
||||
StringBuilder resultMsg = new StringBuilder();
|
||||
boolean resSucc = true;
|
||||
// ds不为空说明传入的是datasourceKey
|
||||
for (MappingConfig configTmp : hbaseMapping.values()) {
|
||||
// 取所有的destination为task的配置
|
||||
if (configTmp.getDestination().equals(task)) {
|
||||
DataSource dataSource = DatasourceConfig.DATA_SOURCES.get(configTmp.getDataSourceKey());
|
||||
if (dataSource == null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (resultMsg.length() > 0) {
|
||||
etlResult.setSucceeded(resSucc);
|
||||
if (resSucc) {
|
||||
etlResult.setResultMessage(resultMsg.toString());
|
||||
EtlResult etlRes = HbaseEtlService.importData(dataSource, hbaseTemplate, configTmp, params);
|
||||
if (!etlRes.getSucceeded()) {
|
||||
resSucc = false;
|
||||
resultMsg.append(etlRes.getErrorMessage()).append("\n");
|
||||
} else {
|
||||
etlResult.setErrorMessage(resultMsg.toString());
|
||||
resultMsg.append(etlRes.getResultMessage()).append("\n");
|
||||
}
|
||||
return etlResult;
|
||||
}
|
||||
}
|
||||
if (resultMsg.length() > 0) {
|
||||
etlResult.setSucceeded(resSucc);
|
||||
if (resSucc) {
|
||||
etlResult.setResultMessage(resultMsg.toString());
|
||||
} else {
|
||||
etlResult.setErrorMessage(resultMsg.toString());
|
||||
}
|
||||
return etlResult;
|
||||
}
|
||||
}
|
||||
etlResult.setSucceeded(false);
|
||||
etlResult.setErrorMessage("Task not found");
|
||||
@@ -139,7 +197,7 @@ public class HbaseAdapter implements OuterAdapter {
|
||||
String hbaseTable = config.getHbaseMapping().getHbaseTable();
|
||||
long rowCount = 0L;
|
||||
try {
|
||||
HTable table = (HTable) conn.getTable(TableName.valueOf(hbaseTable));
|
||||
HTable table = (HTable) hbaseTemplate.getConnection().getTable(TableName.valueOf(hbaseTable));
|
||||
Scan scan = new Scan();
|
||||
scan.setFilter(new FirstKeyOnlyFilter());
|
||||
ResultScanner resultScanner = table.getScanner(scan);
|
||||
@@ -157,12 +215,13 @@ public class HbaseAdapter implements OuterAdapter {
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (configMonitor != null) {
|
||||
configMonitor.destroy();
|
||||
}
|
||||
try {
|
||||
hbaseTemplate.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +229,7 @@ public class HbaseAdapter implements OuterAdapter {
|
||||
public String getDestination(String task) {
|
||||
MappingConfig config = hbaseMapping.get(task);
|
||||
if (config != null && config.getHbaseMapping() != null) {
|
||||
return config.getHbaseMapping().getDestination();
|
||||
return config.getDestination();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,15 @@ import java.util.*;
|
||||
*/
|
||||
public class MappingConfig {
|
||||
|
||||
private String dataSourceKey; // 数据源key
|
||||
private String dataSourceKey; // 数据源key
|
||||
|
||||
private HbaseMapping hbaseMapping; // hbase映射配置
|
||||
private String outerAdapterKey; // adapter key
|
||||
|
||||
private String groupId; // groupId
|
||||
|
||||
private String destination; // canal实例或MQ的topic
|
||||
|
||||
private HbaseMapping hbaseMapping; // hbase映射配置
|
||||
|
||||
public String getDataSourceKey() {
|
||||
return dataSourceKey;
|
||||
@@ -22,6 +28,30 @@ public class MappingConfig {
|
||||
this.dataSourceKey = dataSourceKey;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(String groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getOuterAdapterKey() {
|
||||
return outerAdapterKey;
|
||||
}
|
||||
|
||||
public void setOuterAdapterKey(String outerAdapterKey) {
|
||||
this.outerAdapterKey = outerAdapterKey;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public HbaseMapping getHbaseMapping() {
|
||||
return hbaseMapping;
|
||||
}
|
||||
@@ -66,6 +96,7 @@ public class MappingConfig {
|
||||
public static class ColumnItem {
|
||||
|
||||
private boolean isRowKey = false;
|
||||
private Integer rowKeyLen;
|
||||
private String column;
|
||||
private String family;
|
||||
private String qualifier;
|
||||
@@ -79,6 +110,14 @@ public class MappingConfig {
|
||||
isRowKey = rowKey;
|
||||
}
|
||||
|
||||
public Integer getRowKeyLen() {
|
||||
return rowKeyLen;
|
||||
}
|
||||
|
||||
public void setRowKeyLen(Integer rowKeyLen) {
|
||||
this.rowKeyLen = rowKeyLen;
|
||||
}
|
||||
|
||||
public String getColumn() {
|
||||
return column;
|
||||
}
|
||||
@@ -143,7 +182,6 @@ public class MappingConfig {
|
||||
public static class HbaseMapping {
|
||||
|
||||
private Mode mode = Mode.STRING; // hbase默认转换格式
|
||||
private String destination; // canal实例或MQ的topic
|
||||
private String database; // 数据库名或schema名
|
||||
private String table; // 表面名
|
||||
private String hbaseTable; // hbase表名
|
||||
@@ -169,14 +207,6 @@ public class MappingConfig {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public String getDatabase() {
|
||||
return database;
|
||||
}
|
||||
@@ -263,7 +293,16 @@ public class MappingConfig {
|
||||
ColumnItem columnItem = new ColumnItem();
|
||||
columnItem.setColumn(columnField.getKey());
|
||||
columnItem.setType(type);
|
||||
if ("rowKey".equalsIgnoreCase(field)) {
|
||||
if (field != null && field.toUpperCase().startsWith("ROWKEY")) {
|
||||
int idx = field.toUpperCase().indexOf("LEN:");
|
||||
if (idx > -1) {
|
||||
String len = field.substring(idx + 4);
|
||||
try {
|
||||
columnItem.setRowKeyLen(Integer.parseInt(len));
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
columnItem.setRowKey(true);
|
||||
rowKeyColumn = columnItem;
|
||||
} else {
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
package com.alibaba.otter.canal.client.adapter.hbase.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.AdapterConfigs;
|
||||
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||
|
||||
/**
|
||||
* HBase表映射配置加载器
|
||||
@@ -23,127 +18,34 @@ import com.alibaba.otter.canal.client.adapter.support.AdapterConfigs;
|
||||
*/
|
||||
public class MappingConfigLoader {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(MappingConfigLoader.class);
|
||||
|
||||
private static final String BASE_PATH = "hbase";
|
||||
private static Logger logger = LoggerFactory.getLogger(MappingConfigLoader.class);
|
||||
|
||||
/**
|
||||
* 加载HBase表映射配置
|
||||
*
|
||||
*
|
||||
* @return 配置名/配置文件名--对象
|
||||
*/
|
||||
public static Map<String, MappingConfig> load() {
|
||||
logger.info("## Start loading mapping config ... ");
|
||||
public static Map<String, MappingConfig> load(Properties envProperties) {
|
||||
logger.info("## Start loading hbase mapping config ... ");
|
||||
|
||||
Map<String, MappingConfig> result = new LinkedHashMap<>();
|
||||
|
||||
Collection<String> configs = AdapterConfigs.get("hbase");
|
||||
for (String c : configs) {
|
||||
if (c == null) {
|
||||
continue;
|
||||
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("hbase");
|
||||
configContentMap.forEach((fileName, content) -> {
|
||||
MappingConfig config = YmlConfigBinder
|
||||
.bindYmlToObj(null, content, MappingConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
c = c.trim();
|
||||
if (c.equals("") || c.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MappingConfig config;
|
||||
String configContent = null;
|
||||
|
||||
if (c.endsWith(".yml")) {
|
||||
configContent = readConfigContent(BASE_PATH + "/" + c);
|
||||
}
|
||||
|
||||
// 简单配置database.table@datasourcekey?rowKey=key1,key2
|
||||
if (StringUtils.isEmpty(configContent)) {
|
||||
String[] mapping = c.split("\\?");
|
||||
String params = mapping.length == 2 ? mapping[1] : null;
|
||||
String rowKey = null;
|
||||
String srcMeta = mapping[0];
|
||||
//
|
||||
if (params != null) {
|
||||
for (String entry : params.split("&")) {
|
||||
if ("rowKey".equals(entry.split("=")[0])) {
|
||||
rowKey = entry.split("=")[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
String dsKey = srcMeta.split("@").length == 2 ? srcMeta.split("@")[1] : null;
|
||||
String[] dbTable;
|
||||
if (dsKey == null) {
|
||||
dbTable = srcMeta.split("\\.");
|
||||
|
||||
} else {
|
||||
dbTable = srcMeta.split("@")[0].split("\\.");
|
||||
}
|
||||
|
||||
if (dbTable.length == 2) {
|
||||
config = new MappingConfig();
|
||||
|
||||
MappingConfig.HbaseMapping hbaseMapping = new MappingConfig.HbaseMapping();
|
||||
hbaseMapping.setHbaseTable(dbTable[0].toUpperCase() + "." + dbTable[1].toUpperCase());
|
||||
hbaseMapping.setAutoCreateTable(true);
|
||||
hbaseMapping.setDatabase(dbTable[0]);
|
||||
hbaseMapping.setTable(dbTable[1]);
|
||||
hbaseMapping.setMode(MappingConfig.Mode.PHOENIX);
|
||||
hbaseMapping.setRowKey(rowKey);
|
||||
// 有定义rowKey
|
||||
if (rowKey != null) {
|
||||
MappingConfig.ColumnItem columnItem = new MappingConfig.ColumnItem();
|
||||
columnItem.setRowKey(true);
|
||||
columnItem.setColumn(rowKey);
|
||||
hbaseMapping.setRowKeyColumn(columnItem);
|
||||
}
|
||||
config.setHbaseMapping(hbaseMapping);
|
||||
config.setDataSourceKey(dsKey);
|
||||
|
||||
} else {
|
||||
throw new RuntimeException(String.format("配置项[%s]内容为空, 或格式不符合database.table", c));
|
||||
}
|
||||
|
||||
} else { // 配置文件配置
|
||||
config = new Yaml().loadAs(configContent, MappingConfig.class);
|
||||
}
|
||||
|
||||
try {
|
||||
config.validate();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("ERROR Config: " + c + " " + e.getMessage(), e);
|
||||
throw new RuntimeException("ERROR load Config: " + fileName + " " + e.getMessage(), e);
|
||||
}
|
||||
result.put(c, config);
|
||||
}
|
||||
result.put(fileName, config);
|
||||
});
|
||||
|
||||
logger.info("## Mapping config loaded");
|
||||
logger.info("## Hbase mapping config loaded");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String readConfigContent(String config) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
// 先取本地文件,再取类路径
|
||||
File configFile = new File("config/" + config);
|
||||
if (configFile.exists()) {
|
||||
in = new FileInputStream(configFile);
|
||||
} else {
|
||||
in = MappingConfigLoader.class.getClassLoader().getResourceAsStream(config);
|
||||
}
|
||||
if (in == null) {
|
||||
throw new RuntimeException("Config file not found.");
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[in.available()];
|
||||
in.read(bytes);
|
||||
return new String(bytes, "UTF-8");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Read ds-config.yml or hbase-mappings.conf error. ", e);
|
||||
} finally {
|
||||
try {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.alibaba.otter.canal.client.adapter.hbase.monitor;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.io.filefilter.FileFilterUtils;
|
||||
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
|
||||
import org.apache.commons.io.monitor.FileAlterationMonitor;
|
||||
import org.apache.commons.io.monitor.FileAlterationObserver;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.HbaseAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
public class HbaseConfigMonitor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HbaseConfigMonitor.class);
|
||||
|
||||
private static final String adapterName = "hbase";
|
||||
|
||||
private HbaseAdapter hbaseAdapter;
|
||||
|
||||
private Properties envProperties;
|
||||
|
||||
private FileAlterationMonitor fileMonitor;
|
||||
|
||||
public void init(HbaseAdapter hbaseAdapter, Properties envProperties) {
|
||||
this.hbaseAdapter = hbaseAdapter;
|
||||
this.envProperties = envProperties;
|
||||
File confDir = Util.getConfDirPath(adapterName);
|
||||
try {
|
||||
FileAlterationObserver observer = new FileAlterationObserver(confDir,
|
||||
FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter("yml")));
|
||||
FileListener listener = new FileListener();
|
||||
observer.addListener(listener);
|
||||
fileMonitor = new FileAlterationMonitor(3000, observer);
|
||||
fileMonitor.start();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
try {
|
||||
fileMonitor.stop();
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private class FileListener extends FileAlterationListenerAdaptor {
|
||||
|
||||
@Override
|
||||
public void onFileCreate(File file) {
|
||||
super.onFileCreate(file);
|
||||
try {
|
||||
// 加载新增的配置文件
|
||||
String configContent = MappingConfigsLoader.loadConfig(adapterName + File.separator + file.getName());
|
||||
MappingConfig config = YmlConfigBinder
|
||||
.bindYmlToObj(null, configContent, MappingConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
config.validate();
|
||||
addConfigToCache(file, config);
|
||||
|
||||
logger.info("Add a new hbase mapping config: {} to canal adapter", file.getName());
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileChange(File file) {
|
||||
super.onFileChange(file);
|
||||
|
||||
try {
|
||||
if (hbaseAdapter.getHbaseMapping().containsKey(file.getName())) {
|
||||
// 加载配置文件
|
||||
String configContent = MappingConfigsLoader
|
||||
.loadConfig(adapterName + File.separator + file.getName());
|
||||
if (configContent == null) {
|
||||
onFileDelete(file);
|
||||
return;
|
||||
}
|
||||
MappingConfig config = YmlConfigBinder
|
||||
.bindYmlToObj(null, configContent, MappingConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
config.validate();
|
||||
if (hbaseAdapter.getHbaseMapping().containsKey(file.getName())) {
|
||||
deleteConfigFromCache(file);
|
||||
}
|
||||
addConfigToCache(file, config);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileDelete(File file) {
|
||||
super.onFileDelete(file);
|
||||
|
||||
try {
|
||||
if (hbaseAdapter.getHbaseMapping().containsKey(file.getName())) {
|
||||
deleteConfigFromCache(file);
|
||||
|
||||
logger.info("Delete a hbase mapping config: {} of canal adapter", file.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addConfigToCache(File file, MappingConfig config) {
|
||||
hbaseAdapter.getHbaseMapping().put(file.getName(), config);
|
||||
Map<String, MappingConfig> configMap = hbaseAdapter.getMappingConfigCache()
|
||||
.computeIfAbsent(StringUtils.trimToEmpty(config.getDestination()) + "."
|
||||
+ config.getHbaseMapping().getDatabase() + "." + config.getHbaseMapping().getTable(),
|
||||
k1 -> new HashMap<>());
|
||||
configMap.put(file.getName(), config);
|
||||
}
|
||||
|
||||
private void deleteConfigFromCache(File file) {
|
||||
|
||||
hbaseAdapter.getHbaseMapping().remove(file.getName());
|
||||
for (Map<String, MappingConfig> configMap : hbaseAdapter.getMappingConfigCache().values()) {
|
||||
if (configMap != null) {
|
||||
configMap.remove(file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package com.alibaba.otter.canal.client.adapter.hbase.service;
|
||||
|
||||
import java.sql.*;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.Timestamp;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@@ -19,9 +17,15 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.*;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.HRow;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.HbaseTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.PhType;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.PhTypeUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.Type;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.TypeUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.JdbcTypeUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
/**
|
||||
@@ -34,47 +38,9 @@ public class HbaseEtlService {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(HbaseEtlService.class);
|
||||
|
||||
public static Object sqlRS(DataSource ds, String sql, Function<ResultSet, Object> fun) throws SQLException {
|
||||
Connection conn = null;
|
||||
Statement stmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = ds.getConnection();
|
||||
stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
|
||||
stmt.setFetchSize(Integer.MIN_VALUE);
|
||||
rs = stmt.executeQuery(sql);
|
||||
return fun.apply(rs);
|
||||
} finally {
|
||||
if (rs != null) {
|
||||
try {
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
if (stmt != null) {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.close();
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
rs = null;
|
||||
stmt = null;
|
||||
conn = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 建表
|
||||
*
|
||||
*
|
||||
* @param hbaseTemplate
|
||||
* @param config
|
||||
*/
|
||||
@@ -93,7 +59,7 @@ public class HbaseEtlService {
|
||||
|
||||
/**
|
||||
* 导入数据
|
||||
*
|
||||
*
|
||||
* @param ds 数据源
|
||||
* @param hbaseTemplate hbaseTemplate
|
||||
* @param config 配置
|
||||
@@ -138,7 +104,7 @@ public class HbaseEtlService {
|
||||
if (params != null && params.size() == 1 && hbaseMapping.getEtlCondition() == null) {
|
||||
AtomicBoolean stExists = new AtomicBoolean(false);
|
||||
// 验证是否有SYS_TIME字段
|
||||
sqlRS(ds, sql, rs -> {
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
try {
|
||||
ResultSetMetaData rsmd = rs.getMetaData();
|
||||
int cnt = rsmd.getColumnCount();
|
||||
@@ -169,7 +135,7 @@ public class HbaseEtlService {
|
||||
|
||||
// 获取总数
|
||||
String countSql = "SELECT COUNT(1) FROM ( " + sql + ") _CNT ";
|
||||
long cnt = (Long) sqlRS(ds, countSql, rs -> {
|
||||
long cnt = (Long) Util.sqlRS(ds, countSql, rs -> {
|
||||
Long count = null;
|
||||
try {
|
||||
if (rs.next()) {
|
||||
@@ -185,8 +151,7 @@ public class HbaseEtlService {
|
||||
if (cnt >= 10000) {
|
||||
int threadCount = 3;
|
||||
long perThreadCnt = cnt / threadCount;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
List<Future<Boolean>> futures = new ArrayList<>(threadCount);
|
||||
ExecutorService executor = Util.newFixedThreadPool(threadCount, 5000L);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
long offset = i * perThreadCnt;
|
||||
Long size = null;
|
||||
@@ -199,22 +164,20 @@ public class HbaseEtlService {
|
||||
} else {
|
||||
sqlFinal = sql + " LIMIT " + offset + "," + cnt;
|
||||
}
|
||||
Future<Boolean> future = executor
|
||||
.submit(() -> executeSqlImport(ds, sqlFinal, hbaseMapping, hbaseTemplate, successCount, errMsg));
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
for (Future<Boolean> future : futures) {
|
||||
future.get();
|
||||
executor.submit(
|
||||
() -> executeSqlImport(ds, sqlFinal, hbaseMapping, hbaseTemplate, successCount, errMsg));
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
while (!executor.awaitTermination(3, TimeUnit.SECONDS)) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
executeSqlImport(ds, sql, hbaseMapping, hbaseTemplate, successCount, errMsg);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
hbaseMapping.getHbaseTable() + " etl completed in: " + (System.currentTimeMillis() - start) / 1000 + "s!");
|
||||
logger.info(hbaseMapping.getHbaseTable() + " etl completed in: "
|
||||
+ (System.currentTimeMillis() - start) / 1000 + "s!");
|
||||
|
||||
etlResult.setResultMessage("导入HBase表 " + hbaseMapping.getHbaseTable() + " 数据:" + successCount.get() + " 条");
|
||||
} catch (Exception e) {
|
||||
@@ -232,7 +195,7 @@ public class HbaseEtlService {
|
||||
|
||||
/**
|
||||
* 执行导入
|
||||
*
|
||||
*
|
||||
* @param ds
|
||||
* @param sql
|
||||
* @param hbaseMapping
|
||||
@@ -244,7 +207,7 @@ public class HbaseEtlService {
|
||||
private static boolean executeSqlImport(DataSource ds, String sql, MappingConfig.HbaseMapping hbaseMapping,
|
||||
HbaseTemplate hbaseTemplate, AtomicLong successCount, List<String> errMsg) {
|
||||
try {
|
||||
sqlRS(ds, sql, rs -> {
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
int i = 1;
|
||||
|
||||
try {
|
||||
@@ -332,18 +295,49 @@ public class HbaseEtlService {
|
||||
|
||||
byte[] valBytes = Bytes.toBytes(val.toString());
|
||||
if (columnItem.isRowKey()) {
|
||||
row.setRowKey(valBytes);
|
||||
if (columnItem.getRowKeyLen() != null) {
|
||||
valBytes = Bytes.toBytes(limitLenNum(columnItem.getRowKeyLen(), val));
|
||||
row.setRowKey(valBytes);
|
||||
} else {
|
||||
row.setRowKey(valBytes);
|
||||
}
|
||||
} else {
|
||||
row.addCell(columnItem.getFamily(), columnItem.getQualifier(), valBytes);
|
||||
}
|
||||
} else {
|
||||
PhType phType = PhType.getType(columnItem.getType());
|
||||
if (columnItem.isRowKey()) {
|
||||
row.setRowKey(PhTypeUtil.toBytes(val, phType));
|
||||
} else {
|
||||
row.addCell(columnItem.getFamily(),
|
||||
columnItem.getQualifier(),
|
||||
PhTypeUtil.toBytes(val, phType));
|
||||
if (MappingConfig.Mode.STRING == hbaseMapping.getMode()) {
|
||||
byte[] valBytes = Bytes.toBytes(val.toString());
|
||||
if (columnItem.isRowKey()) {
|
||||
if (columnItem.getRowKeyLen() != null) {
|
||||
valBytes = Bytes.toBytes(limitLenNum(columnItem.getRowKeyLen(), val));
|
||||
}
|
||||
row.setRowKey(valBytes);
|
||||
} else {
|
||||
row.addCell(columnItem.getFamily(), columnItem.getQualifier(), valBytes);
|
||||
}
|
||||
} else if (MappingConfig.Mode.NATIVE == hbaseMapping.getMode()) {
|
||||
Type type = Type.getType(columnItem.getType());
|
||||
if (columnItem.isRowKey()) {
|
||||
if (columnItem.getRowKeyLen() != null) {
|
||||
String v = limitLenNum(columnItem.getRowKeyLen(), val);
|
||||
row.setRowKey(Bytes.toBytes(v));
|
||||
} else {
|
||||
row.setRowKey(TypeUtil.toBytes(val, type));
|
||||
}
|
||||
} else {
|
||||
row.addCell(columnItem.getFamily(),
|
||||
columnItem.getQualifier(),
|
||||
TypeUtil.toBytes(val, type));
|
||||
}
|
||||
} else if (MappingConfig.Mode.PHOENIX == hbaseMapping.getMode()) {
|
||||
PhType phType = PhType.getType(columnItem.getType());
|
||||
if (columnItem.isRowKey()) {
|
||||
row.setRowKey(PhTypeUtil.toBytes(val, phType));
|
||||
} else {
|
||||
row.addCell(columnItem.getFamily(),
|
||||
columnItem.getQualifier(),
|
||||
PhTypeUtil.toBytes(val, phType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,4 +376,16 @@ public class HbaseEtlService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String limitLenNum(int len, Object val) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (val instanceof Number) {
|
||||
return String.format("%0" + len + "d", (Number) ((Number) val).longValue());
|
||||
} else if (val instanceof String) {
|
||||
return String.format("%0" + len + "d", Long.parseLong((String) val));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.apache.hadoop.hbase.util.Bytes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.hbase.support.*;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
@@ -18,38 +20,33 @@ import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
*/
|
||||
public class HbaseSyncService {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private static Logger logger = LoggerFactory.getLogger(HbaseSyncService.class);
|
||||
|
||||
private HbaseTemplate hbaseTemplate; // HBase操作模板
|
||||
private HbaseTemplate hbaseTemplate; // HBase操作模板
|
||||
|
||||
public HbaseSyncService(HbaseTemplate hbaseTemplate){
|
||||
this.hbaseTemplate = hbaseTemplate;
|
||||
}
|
||||
|
||||
public void sync(MappingConfig config, Dml dml) {
|
||||
try {
|
||||
if (config != null) {
|
||||
String type = dml.getType();
|
||||
if (type != null && type.equalsIgnoreCase("INSERT")) {
|
||||
insert(config, dml);
|
||||
} else if (type != null && type.equalsIgnoreCase("UPDATE")) {
|
||||
update(config, dml);
|
||||
} else if (type != null && type.equalsIgnoreCase("DELETE")) {
|
||||
delete(config, dml);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
String res = dml.toString();
|
||||
logger.debug(res);
|
||||
}
|
||||
if (config != null) {
|
||||
String type = dml.getType();
|
||||
if (type != null && type.equalsIgnoreCase("INSERT")) {
|
||||
insert(config, dml);
|
||||
} else if (type != null && type.equalsIgnoreCase("UPDATE")) {
|
||||
update(config, dml);
|
||||
} else if (type != null && type.equalsIgnoreCase("DELETE")) {
|
||||
delete(config, dml);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("DML: {}", JSON.toJSONString(dml, SerializerFeature.WriteMapNullValue));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入操作
|
||||
*
|
||||
*
|
||||
* @param config 配置项
|
||||
* @param dml DML数据
|
||||
*/
|
||||
@@ -101,7 +98,7 @@ public class HbaseSyncService {
|
||||
|
||||
/**
|
||||
* 将Map数据转换为HRow行数据
|
||||
*
|
||||
*
|
||||
* @param hbaseMapping hbase映射配置
|
||||
* @param hRow 行对象
|
||||
* @param data Map数据
|
||||
@@ -132,7 +129,21 @@ public class HbaseSyncService {
|
||||
}
|
||||
} else {
|
||||
if (columnItem.isRowKey()) {
|
||||
// row.put("rowKey", bytes);
|
||||
if (columnItem.getRowKeyLen() != null && entry.getValue() != null) {
|
||||
if (entry.getValue() instanceof Number) {
|
||||
String v = String.format("%0" + columnItem.getRowKeyLen() + "d",
|
||||
((Number) entry.getValue()).longValue());
|
||||
bytes = Bytes.toBytes(v);
|
||||
} else {
|
||||
try {
|
||||
String v = String.format("%0" + columnItem.getRowKeyLen() + "d",
|
||||
Integer.parseInt((String) entry.getValue()));
|
||||
bytes = Bytes.toBytes(v);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
hRow.setRowKey(bytes);
|
||||
} else {
|
||||
hRow.addCell(columnItem.getFamily(), columnItem.getQualifier(), bytes);
|
||||
@@ -145,9 +156,9 @@ public class HbaseSyncService {
|
||||
|
||||
/**
|
||||
* 更新操作
|
||||
*
|
||||
* @param config
|
||||
* @param dml
|
||||
*
|
||||
* @param config 配置对象
|
||||
* @param dml dml对象
|
||||
*/
|
||||
private void update(MappingConfig config, Dml dml) {
|
||||
List<Map<String, Object>> data = dml.getData();
|
||||
@@ -192,14 +203,15 @@ public class HbaseSyncService {
|
||||
Map<String, Object> rowKey = data.get(0);
|
||||
rowKeyBytes = typeConvert(null, hbaseMapping, rowKey.values().iterator().next());
|
||||
} else {
|
||||
rowKeyBytes = typeConvert(rowKeyColumn, hbaseMapping, r.get(rowKeyColumn.getColumn()));
|
||||
rowKeyBytes = getRowKeyBytes(hbaseMapping, rowKeyColumn, r);
|
||||
}
|
||||
if (rowKeyBytes == null) throw new RuntimeException("rowKey值为空");
|
||||
|
||||
Map<String, MappingConfig.ColumnItem> columnItems = hbaseMapping.getColumnItems();
|
||||
HRow hRow = new HRow(rowKeyBytes);
|
||||
for (String updateColumn : old.get(index).keySet()) {
|
||||
if (hbaseMapping.getExcludeColumns() != null && hbaseMapping.getExcludeColumns().contains(updateColumn)) {
|
||||
if (hbaseMapping.getExcludeColumns() != null
|
||||
&& hbaseMapping.getExcludeColumns().contains(updateColumn)) {
|
||||
continue;
|
||||
}
|
||||
MappingConfig.ColumnItem columnItem = columnItems.get(updateColumn);
|
||||
@@ -276,8 +288,7 @@ public class HbaseSyncService {
|
||||
Map<String, Object> rowKey = data.get(0);
|
||||
rowKeyBytes = typeConvert(null, hbaseMapping, rowKey.values().iterator().next());
|
||||
} else {
|
||||
Object val = r.get(rowKeyColumn.getColumn());
|
||||
rowKeyBytes = typeConvert(rowKeyColumn, hbaseMapping, val);
|
||||
rowKeyBytes = getRowKeyBytes(hbaseMapping, rowKeyColumn, r);
|
||||
}
|
||||
if (rowKeyBytes == null) throw new RuntimeException("rowKey值为空");
|
||||
rowKeys.add(rowKeyBytes);
|
||||
@@ -365,7 +376,7 @@ public class HbaseSyncService {
|
||||
|
||||
/**
|
||||
* 根据对应的类型进行转换
|
||||
*
|
||||
*
|
||||
* @param columnItem 列项配置
|
||||
* @param hbaseMapping hbase映射配置
|
||||
* @param value 值
|
||||
@@ -423,4 +434,22 @@ public class HbaseSyncService {
|
||||
return rowKeyValue.toString();
|
||||
}
|
||||
|
||||
private static byte[] getRowKeyBytes(MappingConfig.HbaseMapping hbaseMapping, MappingConfig.ColumnItem rowKeyColumn,
|
||||
Map<String, Object> rowData) {
|
||||
Object val = rowData.get(rowKeyColumn.getColumn());
|
||||
String v = null;
|
||||
if (rowKeyColumn.getRowKeyLen() != null) {
|
||||
if (val instanceof Number) {
|
||||
v = String.format("%0" + rowKeyColumn.getRowKeyLen() + "d", (Number) ((Number) val).longValue());
|
||||
} else if (val instanceof String) {
|
||||
v = String.format("%0" + rowKeyColumn.getRowKeyLen() + "d", Long.parseLong((String) val));
|
||||
}
|
||||
}
|
||||
if (v != null) {
|
||||
return Bytes.toBytes(v);
|
||||
} else {
|
||||
return typeConvert(rowKeyColumn, hbaseMapping, val);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.hadoop.conf.Configuration;
|
||||
import org.apache.hadoop.hbase.HColumnDescriptor;
|
||||
import org.apache.hadoop.hbase.HTableDescriptor;
|
||||
import org.apache.hadoop.hbase.TableName;
|
||||
import org.apache.hadoop.hbase.client.Connection;
|
||||
import org.apache.hadoop.hbase.client.ConnectionFactory;
|
||||
import org.apache.hadoop.hbase.client.Delete;
|
||||
import org.apache.hadoop.hbase.client.HBaseAdmin;
|
||||
import org.apache.hadoop.hbase.client.HTable;
|
||||
@@ -25,16 +27,33 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
public class HbaseTemplate {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private Connection conn;
|
||||
private Configuration hbaseConfig; // hbase配置对象
|
||||
private Connection conn; // hbase连接
|
||||
|
||||
public HbaseTemplate(Connection conn){
|
||||
this.conn = conn;
|
||||
public HbaseTemplate(Configuration hbaseConfig){
|
||||
this.hbaseConfig = hbaseConfig;
|
||||
initConn();
|
||||
}
|
||||
|
||||
private void initConn() {
|
||||
try {
|
||||
this.conn = ConnectionFactory.createConnection(hbaseConfig);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
if (conn == null || conn.isAborted() || conn.isClosed()) {
|
||||
initConn();
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
public boolean tableExists(String tableName) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) getConnection().getAdmin()) {
|
||||
|
||||
return admin.tableExists(TableName.valueOf(tableName));
|
||||
} catch (IOException e) {
|
||||
@@ -43,7 +62,7 @@ public class HbaseTemplate {
|
||||
}
|
||||
|
||||
public void createTable(String tableName, String... familyNames) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) getConnection().getAdmin()) {
|
||||
|
||||
HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(tableName));
|
||||
// 添加列簇
|
||||
@@ -60,7 +79,7 @@ public class HbaseTemplate {
|
||||
}
|
||||
|
||||
public void disableTable(String tableName) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) getConnection().getAdmin()) {
|
||||
admin.disableTable(tableName);
|
||||
} catch (IOException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
@@ -69,7 +88,7 @@ public class HbaseTemplate {
|
||||
}
|
||||
|
||||
public void deleteTable(String tableName) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) conn.getAdmin()) {
|
||||
try (HBaseAdmin admin = (HBaseAdmin) getConnection().getAdmin()) {
|
||||
if (admin.isTableEnabled(tableName)) {
|
||||
disableTable(tableName);
|
||||
}
|
||||
@@ -82,7 +101,7 @@ public class HbaseTemplate {
|
||||
|
||||
/**
|
||||
* 插入一行数据
|
||||
*
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param hRow 行数据对象
|
||||
* @return 是否成功
|
||||
@@ -90,7 +109,7 @@ public class HbaseTemplate {
|
||||
public Boolean put(String tableName, HRow hRow) {
|
||||
boolean flag = false;
|
||||
try {
|
||||
HTable table = (HTable) conn.getTable(TableName.valueOf(tableName));
|
||||
HTable table = (HTable) getConnection().getTable(TableName.valueOf(tableName));
|
||||
Put put = new Put(hRow.getRowKey());
|
||||
for (HRow.HCell hCell : hRow.getCells()) {
|
||||
put.addColumn(Bytes.toBytes(hCell.getFamily()), Bytes.toBytes(hCell.getQualifier()), hCell.getValue());
|
||||
@@ -99,6 +118,7 @@ public class HbaseTemplate {
|
||||
flag = true;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return flag;
|
||||
|
||||
@@ -106,7 +126,7 @@ public class HbaseTemplate {
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param rows 行数据对象集合
|
||||
* @return 是否成功
|
||||
@@ -114,7 +134,7 @@ public class HbaseTemplate {
|
||||
public Boolean puts(String tableName, List<HRow> rows) {
|
||||
boolean flag = false;
|
||||
try {
|
||||
HTable table = (HTable) conn.getTable(TableName.valueOf(tableName));
|
||||
HTable table = (HTable) getConnection().getTable(TableName.valueOf(tableName));
|
||||
List<Put> puts = new ArrayList<>();
|
||||
for (HRow hRow : rows) {
|
||||
Put put = new Put(hRow.getRowKey());
|
||||
@@ -131,13 +151,14 @@ public class HbaseTemplate {
|
||||
flag = true;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除数据
|
||||
*
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param rowKeys rowKey集合
|
||||
* @return 是否成功
|
||||
@@ -145,7 +166,7 @@ public class HbaseTemplate {
|
||||
public Boolean deletes(String tableName, Set<byte[]> rowKeys) {
|
||||
boolean flag = false;
|
||||
try {
|
||||
HTable table = (HTable) conn.getTable(TableName.valueOf(tableName));
|
||||
HTable table = (HTable) getConnection().getTable(TableName.valueOf(tableName));
|
||||
List<Delete> deletes = new ArrayList<>();
|
||||
for (byte[] rowKey : rowKeys) {
|
||||
Delete delete = new Delete(rowKey);
|
||||
@@ -157,7 +178,14 @@ public class HbaseTemplate {
|
||||
flag = true;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import java.sql.Timestamp;
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.hadoop.hbase.util.Bytes;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import com.google.common.math.LongMath;
|
||||
|
||||
/**
|
||||
@@ -130,8 +130,10 @@ public class PhTypeUtil {
|
||||
} else if (b[0] == 0) {
|
||||
v = false;
|
||||
}
|
||||
} else if (phType == PhType.TIME || phType == PhType.DATE) {
|
||||
v = new Date(decodeLong(b, 0));
|
||||
} else if (phType == PhType.DATE) {
|
||||
v = new java.sql.Date(decodeLong(b, 0));
|
||||
} else if (phType == PhType.TIME) {
|
||||
v = new java.sql.Time(decodeLong(b, 0));
|
||||
} else if (phType == PhType.TIMESTAMP) {
|
||||
long millisDeserialized = decodeLong(b, 0);
|
||||
Timestamp ts = new Timestamp(millisDeserialized);
|
||||
@@ -389,7 +391,7 @@ public class PhTypeUtil {
|
||||
String dateStr = (String) v;
|
||||
Date date;
|
||||
try {
|
||||
date = parseDatetime(dateStr);
|
||||
date = Util.parseDate(dateStr);
|
||||
if (date != null) {
|
||||
encodeLong(date.getTime(), b, 0);
|
||||
}
|
||||
@@ -418,7 +420,7 @@ public class PhTypeUtil {
|
||||
String dateStr = (String) v;
|
||||
Date date;
|
||||
try {
|
||||
date = parseDatetime(dateStr);
|
||||
date = Util.parseDate(dateStr);
|
||||
if (date != null) {
|
||||
encodeUnsignedLong(date.getTime(), b, 0);
|
||||
}
|
||||
@@ -599,22 +601,9 @@ public class PhTypeUtil {
|
||||
|
||||
private static void checkForSufficientLength(byte[] b, int offset, int requiredLength) {
|
||||
if (b.length < offset + requiredLength) {
|
||||
throw new RuntimeException("Expected length of at least " + requiredLength + " bytes, but had "
|
||||
+ (b.length - offset));
|
||||
throw new RuntimeException(
|
||||
"Expected length of at least " + requiredLength + " bytes, but had " + (b.length - offset));
|
||||
}
|
||||
}
|
||||
|
||||
private static Date parseDatetime(String dateStr) {
|
||||
Date date = null;
|
||||
int len = dateStr.length();
|
||||
if (len == 10 && dateStr.charAt(4) == '-' && dateStr.charAt(7) == '-') {
|
||||
date = new DateTime(dateStr).toDate();
|
||||
} else if (len == 8 && dateStr.charAt(2) == ':' && dateStr.charAt(5) == ':') {
|
||||
date = new DateTime("T" + dateStr).toDate();
|
||||
} else if (len >= 19 && dateStr.charAt(4) == '-' && dateStr.charAt(7) == '-' && dateStr.charAt(13) == ':'
|
||||
&& dateStr.charAt(16) == ':') {
|
||||
date = new DateTime(dateStr.replace(" ", "T")).toDate();
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
hbaseMapping:
|
||||
mode: PHOENIX #NATIVE #STRING
|
||||
destination: example
|
||||
mode: STRING #NATIVE #PHOENIX
|
||||
database: mytest # 数据库名
|
||||
table: person2 # 数据库表名
|
||||
hbaseTable: MYTEST.PERSON2 # HBase表名
|
||||
@@ -11,14 +12,14 @@ hbaseMapping:
|
||||
#rowKey: id,type # 复合字段rowKey不能和columns中的rowKey重复
|
||||
columns:
|
||||
# 数据库字段:HBase对应字段
|
||||
id: ROWKEY$UNSIGNED_LONG
|
||||
id: ROWKEY LEN:15
|
||||
name: NAME
|
||||
email: EMAIL
|
||||
type: $DECIMAL
|
||||
c_time: C_TIME$UNSIGNED_TIMESTAMP
|
||||
birthday: BIRTHDAY$DATE
|
||||
excludeColumns:
|
||||
- lat # 忽略字段
|
||||
type:
|
||||
c_time: C_TIME
|
||||
birthday: BIRTHDAY
|
||||
# excludeColumns:
|
||||
# - lat # 忽略字段
|
||||
|
||||
# -- NATIVE类型
|
||||
# $DEFAULT
|
||||
@@ -57,4 +58,4 @@ hbaseMapping:
|
||||
# $UNSIGNED_TIMESTAMP 对应PHOENIX里的UNSIGNED_TIMESTAMP 12字节
|
||||
# $VARCHAR 对应PHOENIX里的VARCHAR 动态长度
|
||||
# $VARBINARY 对应PHOENIX里的VARBINARY 动态长度
|
||||
# $DECIMAL 对应PHOENIX里的DECIMAL 动态长度
|
||||
# $DECIMAL 对应PHOENIX里的DECIMAL 动态长度
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.1</version>
|
||||
<version>1.1.3</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
@@ -32,7 +32,7 @@
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal.client</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<version>1.1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
@@ -46,7 +46,12 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-context</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
@@ -66,6 +71,28 @@
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- jdbc -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.40</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.1.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.oracle</groupId>
|
||||
<artifactId>ojdbc6</artifactId>
|
||||
<version>11.2.0.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<version>7.0.0.jre8</version>
|
||||
</dependency>
|
||||
|
||||
<!-- outer adapter jar with dependencies-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
@@ -78,7 +105,7 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<optional>true</optional>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
@@ -91,62 +118,38 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<optional>true</optional>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.elasticsearch</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>*</artifactId>
|
||||
<groupId>*</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.rdb</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>*</artifactId>
|
||||
<groupId>*</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>canal-adapter-launcher</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>2.0.1.RELEASE</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.hbase</artifactId>
|
||||
</exclude>
|
||||
<exclude>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.logger</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<copy todir="${project.basedir}/target/config" overwrite="true">
|
||||
<fileset dir="${project.basedir}/src/main/resources" erroronmissingdir="true">
|
||||
<include name="*.yml" />
|
||||
</fileset>
|
||||
</copy>
|
||||
<copy todir="${project.basedir}/target/config/hbase" overwrite="true">
|
||||
<fileset dir="${project.basedir}/../hbase/src/main/resources/hbase" erroronmissingdir="true">
|
||||
<include name="*.yml" />
|
||||
</fileset>
|
||||
</copy>
|
||||
</tasks>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
@@ -160,15 +163,81 @@
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeClassifiers>jar-with-dependencies</includeClassifiers>
|
||||
<outputDirectory>${project.basedir}/target/lib</outputDirectory>
|
||||
<outputDirectory>${project.basedir}/target/canal-adapter/plugin</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>assemble</id>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<phase>package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<attach>false</attach>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<classpathPrefix>lib/</classpathPrefix>
|
||||
<mainClass>com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<excludes>
|
||||
<exclude>**/*.properties</exclude>
|
||||
<exclude>**/*.xml</exclude>
|
||||
<exclude>**/*.yml</exclude>
|
||||
<exclude>static/**</exclude>
|
||||
<exclude>templates/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
<property>
|
||||
<name>env</name>
|
||||
<value>!release</value>
|
||||
</property>
|
||||
</activation>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>${basedir}/src/main/assembly/dev.xml</descriptor>
|
||||
</descriptors>
|
||||
<finalName>canal-adapter</finalName>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<activation>
|
||||
@@ -183,22 +252,12 @@
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<descriptors>
|
||||
<descriptor>${basedir}/src/main/assembly/release.xml</descriptor>
|
||||
</descriptors>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
<finalName>canal.adapter-${project.version}</finalName>
|
||||
<outputDirectory>${project.basedir}/../../target</outputDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
69
client-adapter/launcher/src/main/assembly/dev.xml
Normal file
69
client-adapter/launcher/src/main/assembly/dev.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
|
||||
<id>dist</id>
|
||||
<formats>
|
||||
<format>dir</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>.</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>README*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>./src/main/bin</directory>
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
<fileMode>0755</fileMode>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>./src/main/resources</directory>
|
||||
<outputDirectory>/conf</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../elasticsearch/src/main/resources/es</directory>
|
||||
<outputDirectory>/conf/es</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../hbase/src/main/resources/hbase</directory>
|
||||
<outputDirectory>/conf/hbase</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../rdb/src/main/resources/</directory>
|
||||
<outputDirectory>/conf</outputDirectory>
|
||||
<excludes>
|
||||
<exclude>META-INF/**</exclude>
|
||||
</excludes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target</directory>
|
||||
<outputDirectory>logs</outputDirectory>
|
||||
<excludes>
|
||||
<exclude>**/*</exclude>
|
||||
</excludes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
<dependencySets>
|
||||
<dependencySet>
|
||||
<outputDirectory>lib</outputDirectory>
|
||||
<excludes>
|
||||
<exclude>junit:junit</exclude>
|
||||
</excludes>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
</assembly>
|
||||
@@ -1,34 +1,74 @@
|
||||
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
|
||||
<id>canal-adapter</id>
|
||||
<id>dist</id>
|
||||
<formats>
|
||||
<format>tar.gz</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}</directory>
|
||||
<directory>.</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>*.jar</include>
|
||||
<include>README*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}/lib</directory>
|
||||
<outputDirectory>/lib/</outputDirectory>
|
||||
<directory>./src/main/bin</directory>
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
<includes>
|
||||
<include>*.jar</include>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
<fileMode>0755</fileMode>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>./src/main/resources</directory>
|
||||
<outputDirectory>/conf</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
|
||||
</includes>
|
||||
</fileSet>
|
||||
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}/config</directory>
|
||||
<outputDirectory>/config/</outputDirectory>
|
||||
<directory>../elasticsearch/src/main/resources/es</directory>
|
||||
<outputDirectory>/conf/es</outputDirectory>
|
||||
<includes>
|
||||
<include>**</include>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../hbase/src/main/resources/hbase</directory>
|
||||
<outputDirectory>/conf/hbase</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../rdb/src/main/resources/</directory>
|
||||
<outputDirectory>/conf</outputDirectory>
|
||||
<excludes>
|
||||
<exclude>META-INF/**</exclude>
|
||||
</excludes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target</directory>
|
||||
<outputDirectory>logs</outputDirectory>
|
||||
<excludes>
|
||||
<exclude>**/*</exclude>
|
||||
</excludes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>${project.basedir}/target/canal-adapter/plugin</directory>
|
||||
<outputDirectory>/plugin/</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
<baseDirectory>/</baseDirectory>
|
||||
<dependencySets>
|
||||
<dependencySet>
|
||||
<outputDirectory>lib</outputDirectory>
|
||||
<excludes>
|
||||
<exclude>junit:junit</exclude>
|
||||
</excludes>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
</assembly>
|
||||
5
client-adapter/launcher/src/main/bin/restart.sh
Normal file
5
client-adapter/launcher/src/main/bin/restart.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
sh stop.sh
|
||||
|
||||
sh startup.sh
|
||||
22
client-adapter/launcher/src/main/bin/startup.bat
Executable file
22
client-adapter/launcher/src/main/bin/startup.bat
Executable file
@@ -0,0 +1,22 @@
|
||||
@echo off
|
||||
@if not "%ECHO%" == "" echo %ECHO%
|
||||
@if "%OS%" == "Windows_NT" setlocal
|
||||
|
||||
set ENV_PATH=.\
|
||||
if "%OS%" == "Windows_NT" set ENV_PATH=%~dp0%
|
||||
|
||||
set conf_dir=%ENV_PATH%\..\conf
|
||||
|
||||
set CLASSPATH=%conf_dir%
|
||||
set CLASSPATH=%conf_dir%\..\lib\*;%CLASSPATH%
|
||||
|
||||
set JAVA_MEM_OPTS= -Xms128m -Xmx512m -XX:PermSize=128m
|
||||
set JAVA_OPTS_EXT= -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dapplication.codeset=UTF-8 -Dfile.encoding=UTF-8
|
||||
set ADAPTER_OPTS= -DappName=canal-adapter
|
||||
|
||||
set JAVA_OPTS= %JAVA_MEM_OPTS% %JAVA_OPTS_EXT% %ADAPTER_OPTS%
|
||||
|
||||
set CMD_STR= java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication
|
||||
echo start cmd : %CMD_STR%
|
||||
|
||||
java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication
|
||||
83
client-adapter/launcher/src/main/bin/startup.sh
Normal file
83
client-adapter/launcher/src/main/bin/startup.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
|
||||
current_path=`pwd`
|
||||
case "`uname`" in
|
||||
Linux)
|
||||
bin_abs_path=$(readlink -f $(dirname $0))
|
||||
;;
|
||||
*)
|
||||
bin_abs_path=`cd $(dirname $0); pwd`
|
||||
;;
|
||||
esac
|
||||
base=${bin_abs_path}/..
|
||||
export LANG=en_US.UTF-8
|
||||
export BASE=$base
|
||||
|
||||
if [ -f $base/bin/adapter.pid ] ; then
|
||||
echo "found adapter.pid , Please run stop.sh first ,then startup.sh" 2>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d $base/logs ] ; then
|
||||
mkdir -p $base/logs
|
||||
fi
|
||||
|
||||
## set java path
|
||||
if [ -z "$JAVA" ] ; then
|
||||
JAVA=$(which java)
|
||||
fi
|
||||
|
||||
ALIBABA_JAVA="/usr/alibaba/java/bin/java"
|
||||
TAOBAO_JAVA="/opt/taobao/java/bin/java"
|
||||
if [ -z "$JAVA" ]; then
|
||||
if [ -f $ALIBABA_JAVA ] ; then
|
||||
JAVA=$ALIBABA_JAVA
|
||||
elif [ -f $TAOBAO_JAVA ] ; then
|
||||
JAVA=$TAOBAO_JAVA
|
||||
else
|
||||
echo "Cannot find a Java JDK. Please set either set JAVA or put java (>=1.5) in your PATH." 2>&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$#"
|
||||
in
|
||||
0 )
|
||||
;;
|
||||
2 )
|
||||
if [ "$1" = "debug" ]; then
|
||||
DEBUG_PORT=$2
|
||||
DEBUG_SUSPEND="n"
|
||||
JAVA_DEBUG_OPT="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=$DEBUG_PORT,server=y,suspend=$DEBUG_SUSPEND"
|
||||
fi
|
||||
;;
|
||||
* )
|
||||
echo "THE PARAMETERS MUST BE TWO OR LESS.PLEASE CHECK AGAIN."
|
||||
exit;;
|
||||
esac
|
||||
|
||||
str=`file -L $JAVA | grep 64-bit`
|
||||
if [ -n "$str" ]; then
|
||||
JAVA_OPTS="-server -Xms2048m -Xmx3072m -Xmn1024m -XX:SurvivorRatio=2 -XX:PermSize=96m -XX:MaxPermSize=256m -Xss256k -XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=15 -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError"
|
||||
else
|
||||
JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:MaxPermSize=128m "
|
||||
fi
|
||||
|
||||
JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8"
|
||||
ADAPTER_OPTS="-DappName=canal-adapter"
|
||||
|
||||
for i in $base/lib/*;
|
||||
do CLASSPATH=$i:"$CLASSPATH";
|
||||
done
|
||||
|
||||
CLASSPATH="$base/conf:$CLASSPATH";
|
||||
|
||||
echo "cd to $bin_abs_path for workaround relative path"
|
||||
cd $bin_abs_path
|
||||
|
||||
echo CLASSPATH :$CLASSPATH
|
||||
$JAVA $JAVA_OPTS $JAVA_DEBUG_OPT $ADAPTER_OPTS -classpath .:$CLASSPATH com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication 1>>/dev/null 2>&1 &
|
||||
echo $! > $base/bin/adapter.pid
|
||||
|
||||
echo "cd to $current_path for continue"
|
||||
cd $current_path
|
||||
65
client-adapter/launcher/src/main/bin/stop.sh
Normal file
65
client-adapter/launcher/src/main/bin/stop.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
cygwin=false;
|
||||
linux=false;
|
||||
case "`uname`" in
|
||||
CYGWIN*)
|
||||
cygwin=true
|
||||
;;
|
||||
Linux*)
|
||||
linux=true
|
||||
;;
|
||||
esac
|
||||
|
||||
get_pid() {
|
||||
STR=$1
|
||||
PID=$2
|
||||
if $cygwin; then
|
||||
JAVA_CMD="$JAVA_HOME\bin\java"
|
||||
JAVA_CMD=`cygpath --path --unix $JAVA_CMD`
|
||||
JAVA_PID=`ps |grep $JAVA_CMD |awk '{print $1}'`
|
||||
else
|
||||
if $linux; then
|
||||
if [ ! -z "$PID" ]; then
|
||||
JAVA_PID=`ps -C java -f --width 1000|grep "$STR"|grep "$PID"|grep -v grep|awk '{print $2}'`
|
||||
else
|
||||
JAVA_PID=`ps -C java -f --width 1000|grep "$STR"|grep -v grep|awk '{print $2}'`
|
||||
fi
|
||||
else
|
||||
if [ ! -z "$PID" ]; then
|
||||
JAVA_PID=`ps aux |grep "$STR"|grep "$PID"|grep -v grep|awk '{print $2}'`
|
||||
else
|
||||
JAVA_PID=`ps aux |grep "$STR"|grep -v grep|awk '{print $2}'`
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo $JAVA_PID;
|
||||
}
|
||||
|
||||
base=`dirname $0`/..
|
||||
pidfile=$base/bin/adapter.pid
|
||||
if [ ! -f "$pidfile" ];then
|
||||
echo "canal-adapter is not running. exists"
|
||||
exit
|
||||
fi
|
||||
|
||||
pid=`cat $pidfile`
|
||||
if [ "$pid" == "" ] ; then
|
||||
pid=`get_pid "appName=canal-adapter"`
|
||||
fi
|
||||
|
||||
echo -e "`hostname`: stopping canal $pid ... "
|
||||
kill $pid
|
||||
|
||||
LOOPS=0
|
||||
while (true);
|
||||
do
|
||||
gpid=`get_pid "appName=canal-adapter" "$pid"`
|
||||
if [ "$gpid" == "" ] ; then
|
||||
echo "Oook! cost:$LOOPS"
|
||||
`rm $pidfile`
|
||||
break;
|
||||
fi
|
||||
let LOOPS=LOOPS+1
|
||||
sleep 1
|
||||
done
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.alibaba.otter.canal.adapter.launcher;
|
||||
|
||||
import org.springframework.boot.Banner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
|
||||
/**
|
||||
* 启动入口
|
||||
@@ -13,6 +14,8 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
public class CanalAdapterApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(CanalAdapterApplication.class).run(args);
|
||||
SpringApplication application = new SpringApplication(CanalAdapterApplication.class);
|
||||
application.setBannerMode(Banner.Mode.OFF);
|
||||
application.run(args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class EtlLock {
|
||||
|
||||
private static final Map<String, InterProcessMutex> DISTRIBUTED_LOCK = new ConcurrentHashMap<>();
|
||||
|
||||
private static Mode mode = Mode.LOCAL;
|
||||
private Mode mode = Mode.LOCAL;
|
||||
|
||||
@Resource
|
||||
private CuratorClient curatorClient;
|
||||
|
||||
@@ -34,7 +34,7 @@ public class SyncSwitch {
|
||||
|
||||
private static final Map<String, BooleanMutex> DISTRIBUTED_LOCK = new ConcurrentHashMap<>();
|
||||
|
||||
private static Mode mode = Mode.LOCAL;
|
||||
private Mode mode = Mode.LOCAL;
|
||||
|
||||
@Resource
|
||||
private AdapterCanalConfig adapterCanalConfig;
|
||||
@@ -64,11 +64,12 @@ public class SyncSwitch {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private synchronized void startListen(String destination, BooleanMutex mutex) {
|
||||
try {
|
||||
String path = SYN_SWITCH_ZK_NODE + destination;
|
||||
CuratorFramework curator = curatorClient.getCurator();
|
||||
final NodeCache nodeCache = new NodeCache(curator, path);
|
||||
NodeCache nodeCache = new NodeCache(curator, path);
|
||||
nodeCache.start();
|
||||
nodeCache.getListenable().addListener(() -> initMutex(curator, destination, mutex));
|
||||
} catch (Exception e) {
|
||||
@@ -164,20 +165,20 @@ public class SyncSwitch {
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean status(String destination) {
|
||||
public boolean status(String destination) {
|
||||
if (mode == Mode.LOCAL) {
|
||||
BooleanMutex mutex = LOCAL_LOCK.get(destination);
|
||||
if (mutex != null) {
|
||||
return mutex.state();
|
||||
} else {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
BooleanMutex mutex = DISTRIBUTED_LOCK.get(destination);
|
||||
if (mutex != null) {
|
||||
return mutex.state();
|
||||
} else {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user