├── .gitignore
├── LICENSE
├── README.md
├── backend
├── README.md
├── backend-architecture.md
├── database
│ ├── 数据库事务与隔离级别.md
│ ├── 数据库常见问题.md
│ ├── 数据库设计规范-mysql.md
│ ├── 数据库连接池.md
│ └── 是否要用redis.md
├── java
│ ├── Java后台服务分层规范.md
│ ├── Java命名规范.md
│ ├── Log中的trace记录.md
│ ├── SpringBoot配置.md
│ ├── how-to-handle-exception.md
│ ├── java-best-practices.md
│ ├── java-code-guideline.md
│ ├── java中stream使用规范.md
│ ├── java异步编程规范.md
│ ├── java数据访问层最佳实践.md
│ ├── 如何使用 Java 根据 Html 生成 PDF 文档.md
│ ├── 如何使用Java根据Excel模板文件绑定数据生成新文件.md
│ ├── 如何使用枚举.md
│ ├── 如何实现通用模块功能并与特定产品业务解耦.md
│ ├── 如何序列化xml格式的数据.md
│ ├── 常见的编码习惯.md
│ ├── 数据字段约定.md
│ └── 时间的存储.md
├── process
│ ├── API设计规范.md
│ ├── basic-service-developer-flow.md
│ ├── java-service-test.md
│ ├── release-guideline.md
│ ├── service-basic-rule.md
│ └── swagger-usage-guideline.md
├── regulation
│ ├── how-to-init-project.md
│ ├── quality_assurance.md
│ ├── tmc-services项目review小记.md
│ └── 后端测试.md
└── resources
│ ├── akka-flowgraph.png
│ ├── java_layer_depencies.png
│ ├── java_project_layer.png
│ ├── jet-workflow-example.png
│ ├── jet-workflow.png
│ ├── release_example.png
│ ├── swagger.jpg
│ └── workflow-vs-microservice.png
├── coding-guide
├── README.md
├── fe-code-review-check-list.md
├── git-workflow.md
├── how-to-control-version.md
├── how-to-handle-error.md
├── how-to-log.md
├── how-to-make-your-code-more-safely.md
├── how-to-review-code.md
├── how-to-write-commit-message.md
├── sample-project-readme.md
└── 如何实现灰度发版-数据库篇.md
├── frontend
├── E2E测试数据的设计策略.md
├── README.md
├── best-practices
│ ├── angular-best-practices.md
│ ├── ionic-project
│ │ ├── ionic-best-practices.md
│ │ ├── ionic-test.md
│ │ └── ionic4
│ │ │ ├── ion-virtual-scroll.md
│ │ │ ├── ionic4 使用热更新插件问题cordova-hot-code-push-plugin.md
│ │ │ └── ionic4-upgrade-issues.md
│ ├── rxjs
│ │ └── retry、retryWhen、catchError、repeat、repeatWhen.md
│ ├── ts-best-practices.md
│ ├── web-project
│ │ ├── images
│ │ │ └── simple-search-page.png
│ │ ├── project-best-practices.md
│ │ ├── simple-query-page.md
│ │ └── 常见坑汇总.md
│ └── 样式书写规范.md
├── code-standards
│ ├── code-documentation.md
│ ├── ide-setup.md
│ ├── sample_dot_ignore
│ └── typescript-coding-standard.md
├── 前端应用解构.md
├── 前端项目实践.md
└── 小程序技术选型:原生 VS Taro.md
├── learning
├── README.md
├── how-to-learn-programming.md
└── programming-tips.md
├── pm
├── README.md
└── how-to-review-product-design.md
├── principles.md
└── team
├── README.md
├── culture.md
└── 企业文化20190705.pptx
/.gitignore:
--------------------------------------------------------------------------------
1 | # ignore generated ionic files
2 |
3 | .sourcemaps/
4 | .sass-cache/
5 | coverage/
6 | hooks/
7 | platforms/
8 | plugins/
9 | www/
10 | resources/android/
11 | resources/ios/
12 | .metals/
13 |
14 | # Logs
15 | logs
16 | npm-debug.log*
17 | yarn-debug.log*
18 | yarn-error.log*
19 |
20 | # Optional npm cache directory
21 | .npm
22 |
23 | # Dependency directories
24 | /node_modules
25 | /jspm_packages
26 | /bower_components
27 |
28 | # Yarn Integrity file
29 | .yarn-integrity
30 |
31 | # Optional eslint cache
32 | .eslintcache
33 |
34 | # dotenv environment variables file(s)
35 | .env
36 | .env.*
37 |
38 | #Build generated
39 | dist/
40 | build/
41 |
42 | # Serverless generated files
43 | .serverless/
44 |
45 | ### SublimeText ###
46 | # cache files for sublime text
47 | *.tmlanguage.cache
48 | *.tmPreferences.cache
49 | *.stTheme.cache
50 |
51 | # workspace files are user-specific
52 | *.sublime-workspace
53 |
54 | # project files should be checked into the repository, unless a significant
55 | # proportion of contributors will probably not be using SublimeText
56 | # *.sublime-project
57 |
58 |
59 | ### VisualStudioCode ###
60 | .vscode/*
61 | !.vscode/settings.json
62 | !.vscode/tasks.json
63 | !.vscode/launch.json
64 | !.vscode/extensions.json
65 |
66 | ### Vim ###
67 | *.sw[a-p]
68 |
69 | ### WebStorm/IntelliJ ###
70 | /.idea
71 | modules.xml
72 | *.ipr
73 | *.iml
74 |
75 |
76 | ### System Files ###
77 | *.DS_Store
78 |
79 | # Windows thumbnail cache files
80 | Thumbs.db
81 | ehthumbs.db
82 | ehthumbs_vista.db
83 |
84 | # Folder config file
85 | Desktop.ini
86 |
87 | # Recycle Bin used on file shares
88 | $RECYCLE.BIN/
89 |
90 | # Thumbnails
91 | ._*
92 |
93 | # Files that might appear in the root of a volume
94 | .DocumentRevisions-V100
95 | .fseventsd
96 | .Spotlight-V100
97 | .TemporaryItems
98 | .Trashes
99 | .VolumeIcon.icns
100 | .com.apple.timemachine.donotpresent
101 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction, and
10 | distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright
13 | owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other entities
16 | that control, are controlled by, or are under common control with that entity.
17 | For the purposes of this definition, "control" means (i) the power, direct or
18 | indirect, to cause the direction or management of such entity, whether by
19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
20 | outstanding shares, or (iii) beneficial ownership of such entity.
21 |
22 | "You" (or "Your") shall mean an individual or Legal Entity exercising
23 | permissions granted by this License.
24 |
25 | "Source" form shall mean the preferred form for making modifications, including
26 | but not limited to software source code, documentation source, and configuration
27 | files.
28 |
29 | "Object" form shall mean any form resulting from mechanical transformation or
30 | translation of a Source form, including but not limited to compiled object code,
31 | generated documentation, and conversions to other media types.
32 |
33 | "Work" shall mean the work of authorship, whether in Source or Object form, made
34 | available under the License, as indicated by a copyright notice that is included
35 | in or attached to the work (an example is provided in the Appendix below).
36 |
37 | "Derivative Works" shall mean any work, whether in Source or Object form, that
38 | is based on (or derived from) the Work and for which the editorial revisions,
39 | annotations, elaborations, or other modifications represent, as a whole, an
40 | original work of authorship. For the purposes of this License, Derivative Works
41 | shall not include works that remain separable from, or merely link (or bind by
42 | name) to the interfaces of, the Work and Derivative Works thereof.
43 |
44 | "Contribution" shall mean any work of authorship, including the original version
45 | of the Work and any modifications or additions to that Work or Derivative Works
46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
47 | by the copyright owner or by an individual or Legal Entity authorized to submit
48 | on behalf of the copyright owner. For the purposes of this definition,
49 | "submitted" means any form of electronic, verbal, or written communication sent
50 | to the Licensor or its representatives, including but not limited to
51 | communication on electronic mailing lists, source code control systems, and
52 | issue tracking systems that are managed by, or on behalf of, the Licensor for
53 | the purpose of discussing and improving the Work, but excluding communication
54 | that is conspicuously marked or otherwise designated in writing by the copyright
55 | owner as "Not a Contribution."
56 |
57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
58 | of whom a Contribution has been received by Licensor and subsequently
59 | incorporated within the Work.
60 |
61 | 2. Grant of Copyright License.
62 |
63 | Subject to the terms and conditions of this License, each Contributor hereby
64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
65 | irrevocable copyright license to reproduce, prepare Derivative Works of,
66 | publicly display, publicly perform, sublicense, and distribute the Work and such
67 | Derivative Works in Source or Object form.
68 |
69 | 3. Grant of Patent License.
70 |
71 | Subject to the terms and conditions of this License, each Contributor hereby
72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
73 | irrevocable (except as stated in this section) patent license to make, have
74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
75 | such license applies only to those patent claims licensable by such Contributor
76 | that are necessarily infringed by their Contribution(s) alone or by combination
77 | of their Contribution(s) with the Work to which such Contribution(s) was
78 | submitted. If You institute patent litigation against any entity (including a
79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
80 | Contribution incorporated within the Work constitutes direct or contributory
81 | patent infringement, then any patent licenses granted to You under this License
82 | for that Work shall terminate as of the date such litigation is filed.
83 |
84 | 4. Redistribution.
85 |
86 | You may reproduce and distribute copies of the Work or Derivative Works thereof
87 | in any medium, with or without modifications, and in Source or Object form,
88 | provided that You meet the following conditions:
89 |
90 | You must give any other recipients of the Work or Derivative Works a copy of
91 | this License; and
92 | You must cause any modified files to carry prominent notices stating that You
93 | changed the files; and
94 | You must retain, in the Source form of any Derivative Works that You distribute,
95 | all copyright, patent, trademark, and attribution notices from the Source form
96 | of the Work, excluding those notices that do not pertain to any part of the
97 | Derivative Works; and
98 | If the Work includes a "NOTICE" text file as part of its distribution, then any
99 | Derivative Works that You distribute must include a readable copy of the
100 | attribution notices contained within such NOTICE file, excluding those notices
101 | that do not pertain to any part of the Derivative Works, in at least one of the
102 | following places: within a NOTICE text file distributed as part of the
103 | Derivative Works; within the Source form or documentation, if provided along
104 | with the Derivative Works; or, within a display generated by the Derivative
105 | Works, if and wherever such third-party notices normally appear. The contents of
106 | the NOTICE file are for informational purposes only and do not modify the
107 | License. You may add Your own attribution notices within Derivative Works that
108 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
109 | provided that such additional attribution notices cannot be construed as
110 | modifying the License.
111 | You may add Your own copyright statement to Your modifications and may provide
112 | additional or different license terms and conditions for use, reproduction, or
113 | distribution of Your modifications, or for any such Derivative Works as a whole,
114 | provided Your use, reproduction, and distribution of the Work otherwise complies
115 | with the conditions stated in this License.
116 |
117 | 5. Submission of Contributions.
118 |
119 | Unless You explicitly state otherwise, any Contribution intentionally submitted
120 | for inclusion in the Work by You to the Licensor shall be under the terms and
121 | conditions of this License, without any additional terms or conditions.
122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
123 | any separate license agreement you may have executed with Licensor regarding
124 | such Contributions.
125 |
126 | 6. Trademarks.
127 |
128 | This License does not grant permission to use the trade names, trademarks,
129 | service marks, or product names of the Licensor, except as required for
130 | reasonable and customary use in describing the origin of the Work and
131 | reproducing the content of the NOTICE file.
132 |
133 | 7. Disclaimer of Warranty.
134 |
135 | Unless required by applicable law or agreed to in writing, Licensor provides the
136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
138 | including, without limitation, any warranties or conditions of TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
140 | solely responsible for determining the appropriateness of using or
141 | redistributing the Work and assume any risks associated with Your exercise of
142 | permissions under this License.
143 |
144 | 8. Limitation of Liability.
145 |
146 | In no event and under no legal theory, whether in tort (including negligence),
147 | contract, or otherwise, unless required by applicable law (such as deliberate
148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
149 | liable to You for damages, including any direct, indirect, special, incidental,
150 | or consequential damages of any character arising as a result of this License or
151 | out of the use or inability to use the Work (including but not limited to
152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
153 | any and all other commercial damages or losses), even if such Contributor has
154 | been advised of the possibility of such damages.
155 |
156 | 9. Accepting Warranty or Additional Liability.
157 |
158 | While redistributing the Work or Derivative Works thereof, You may choose to
159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
160 | other liability obligations and/or rights consistent with this License. However,
161 | in accepting such obligations, You may act only on Your own behalf and on Your
162 | sole responsibility, not on behalf of any other Contributor, and only if You
163 | agree to indemnify, defend, and hold each Contributor harmless for any liability
164 | incurred by, or claims asserted against, such Contributor by reason of your
165 | accepting any such warranty or additional liability.
166 |
167 | END OF TERMS AND CONDITIONS
168 |
169 | APPENDIX: How to apply the Apache License to your work
170 |
171 | To apply the Apache License to your work, attach the following boilerplate
172 | notice, with the fields enclosed by brackets "{}" replaced with your own
173 | identifying information. (Don't include the brackets!) The text should be
174 | enclosed in the appropriate comment syntax for the file format. We also
175 | recommend that a file or class name and description of purpose be included on
176 | the same "printed page" as the copyright notice for easier identification within
177 | third-party archives.
178 |
179 | Copyright 2018 YING
180 |
181 | Licensed under the Apache License, Version 2.0 (the "License");
182 | you may not use this file except in compliance with the License.
183 | You may obtain a copy of the License at
184 |
185 | http://www.apache.org/licenses/LICENSE-2.0
186 |
187 | Unless required by applicable law or agreed to in writing, software
188 | distributed under the License is distributed on an "AS IS" BASIS,
189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190 | See the License for the specific language governing permissions and
191 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 开发文档
2 |
3 | ---
4 |
5 | 备注: **这里只存放可以公开共享的、目前有效的文档,不要存放任何会议记录或具体项目相关的文档。包含有内部项目截屏、计划或人员信息的文档要存在内部私有库。**
6 |
7 | 持续的、高质效的软件开发能力是现代企业的核心竞争力。多年实践让我们意识到以下几点事实:
8 |
9 | - 企业文化不是业务的一个方面,它就是业务。Culture isn't just one aspect of the game, it is the game. (郭士纳,Louis V. Gerstner, IBM 前总裁)
10 | - 思想就是软件。 Mind is software. (老刘)
11 | - 切忌随波逐流。 Only dead fish go with the flow.(西谚)
12 | - 做人如果没有梦想,跟咸鱼有什么分别。 Salted fish has no dream. (星爷)
13 | - 管理的本质是激发善意和潜能。The essence of management is to inspire goodwill and potential.(德鲁克)
14 |
15 | 用思想创造软件,进而改变世界的程序员心怀理想,责任重大,任劳任怨。可是环顾四周,多数软件团队的研发能力相对计算机的巨大潜力和广泛的业务需求有巨大鸿沟,开发效率低,软件的质量令人忧伤。稍感安慰的是半个多世纪的编程历史积累了一些基本原则和最佳实践(best practices)。遵守基于这些原则和最佳实践能大大改善程序员的工作效率和工作氛围。
16 |
17 | ## 出发点
18 |
19 | [程序员工作原则](./principles.md)给出了我们的开发理念和基本原则。这些原则都是为了达成软件开发的二个根本目标:正确的业务逻辑与可维护性。
20 |
21 | 软件开发管理活动,如自动测试要求,代码标准,代码审核,文档标准,github 工作流,需求工作流程,设计工作流程,开发环境配置等等都是基于这些原则来展开。我们明白这些流程、标准、模版、规则不是铁一样的限制,有足够好的理由,任何规定都可以按实际情况来改变或取消。标准化和自动化流程的结果是让开发人员把精力放在最有价值的创造性工作中。
22 |
23 | 工作的高效率来自高标准自我要求和务实的合作精神。企业文化是企业提倡的团队价值观。所有的流程和做事方法都尊从我们提倡的[企业文化](./team/culture.md)。
24 |
25 | ## 文档的创建和维护
26 |
27 | 可以和外部共享的、可以标准化的流程和最佳实践都存放这里。
28 |
29 | 所有的工作都必须符合流程规则要求。如果规则或最佳实践不再适用,要先更新文档再按新流程执行。更新流程和最佳实践是所有开发人员的责任。
30 |
31 | 有很多文档或需要说明的目录应该有一个`README.md`说明文件。这个文件描述了目录下的所有文件并保持更新。当目录下的文件和文件夹超过十个时,需要按分类创建子目录。
32 |
33 | 所有文件的创建和更新需要创建 PR 和通过 Review。然后通知所有相关人员按新规则执行。不影响软件开发活动的简单的笔误更正和内容改善可以直接提交。
34 |
35 | 文档统一采用 Markdown 格式。使用 VS Code 并用 [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)来保证 markdown 文件风格的一致性。
36 |
37 | ## 目录和文件说明
38 |
39 | - [程序员工作原则](./principles.md):我们的工作理念和原则
40 | - [team](./team/README.md): 企业文化。
41 | - [coding-guide](./coding-guide/README.md): 编程指南,包括 Git 工作流、代码审核和如何写日志等。
42 | - [frontend](./frontend/README.md):前端的技术文档。
43 | - [backend](./backend/README.md): 后端的技术文档。
44 | - [backend-and-frontend](./backend-and-frontend/README.md):前后端接口相关的技术文档
45 | - [pm](./pm/README.md): 产品经理和项目管理文档。
46 | - [learning](./learning/README.md): 程序员学习指南。
47 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # 后端技术规范和最佳实践
2 |
3 | 这里是有关后端项目的技术与代码规范。旧的系统采用 Java, JPA/Hibernate。 新系统的架构在[backend architecture](./backend-architecture.md)有描述.
4 |
5 | ## 开发流程
6 |
7 | - [后端服务开发基本流程](./process/basic-service-developer-flow.md): 关于后端各个服务开发的基本流程
8 | - [发版流程](./process/release-guideline.md): 关于发版的规范流程
9 | - [API 设计规范](./process/API设计规范.md): 关于后端 API 的设计规范
10 | - [swagger 使用规约](./process/swagger-usage-guideline.md): 关于如何使用 swagger 生成 api 文档已经 api 文档的风格.
11 | - [java 测试框架及方式](./java-service-test.md): 关于如何进行单元测试和集成测试
12 | - [微服务开发规约](./process/service-basic-rule.md): 微服务开发规约
13 | - [审核 PR](./process/how-to-review-pr.md).
14 |
15 | ## 编码规范
16 |
17 | Scala coding guidelines are in the `code/scala` folder.
18 |
19 | Java coding guidelines are in the `code/java` folder.
20 |
21 | - [Java 代码最佳实践](./code/java/java-best-practices.md): Java 代码惯例用法
22 | - [Java 高级代码规范](./code/java/java-code-guideline.md): 关于复杂场景的 java 编码规约
23 | - [如何处理异常](./code/java/how-to-handle-exception.md): 关于异常处理的方式
24 | - [后端如何写日志](./code/java/如何写日志.md): 关于后端记日志的详细说明
25 | - [Java 异步编程规范](./code/java/java异步编程规范.md): 关于 Java 中异步编程的详细说明
26 | - [Java 命名规范](./code/java/Java命名规范.md): 关于后端 Java 代码的命名规范
27 | - [Java 后台项目如何分层](./code/java/Java后台服务分层规范.md): 关于 Java 后台服务项目如何分层的一些标准
28 |
29 | ## 数据库
30 |
31 | - [数据库设计规范-mysql](./database/数据库设计规范-mysql.md): 关于数据库设计的规范
32 | - [数据库事务与隔离级别](./database/数据库事务与隔离级别.md)
33 | - [数据库连接池](./database/数据库连接池.md)
34 | - [是否使用 Redis](./database/是否使用redis.md)
35 |
--------------------------------------------------------------------------------
/backend/backend-architecture.md:
--------------------------------------------------------------------------------
1 | # 后端技术架构
2 |
3 | 任何技术系统都是为其业务服务。技术是达成业务目标的手段。虽然每个业务系统都强调正确性、可靠性、可维护性、开发效率和灵活性,但是真正明白自己业务系统特点并能做出合适的选择还需要对业务系统和相关技术的有深刻、本质性的理解。
4 |
5 | ## 业务系统特点
6 |
7 | 我们的差旅管理业务有不同形态的客户群体,不同形态的产品,复杂多变的业务流程。产品、客户、业务功能需要灵活的组合并且经常变。即使同一个处理流程,不同的客户也需要各种定制功能。我们的业务系统的二个突出特点是:所有产品几乎都是依赖于国内外的第三方服务:机票,酒店,用车,短信。数据量大,接口繁多,经常有突发流量。系统中充满异步处理和各种错误处理,而且流程需要适应不同客户,同时经常变化。
8 |
9 | 传统的做法是编写一套处理流程,包含所有业务处理任务,每个客户群体,甚至每个客户通过参数定制。再加上产品维度,这种做法会让主流程非常复杂,参数配置会非常多。结果就是不堪重负,难以维护,运行缓慢。
10 |
11 | ## 技术系统需求
12 |
13 | 一个理想的系统的架构就是以尽可能靠近物理世界的方式运作。这样一个系统比较高效而且(表面上)容易理解。现实生活中业务系统在时空上都是以一种总体异步,局部同步的多个体并行方式在工作。特定时空的业务内容包含业务处理和数据两个维度。
14 |
15 | 理想的处理系统复杂性的方法是采用类似于数学的分形(fractal)思想,把系统功能分成可以组合的细颗粒任务,然后以一种一致的方式对不同客户/产品进行任意组合。
16 |
17 | 从数据的角度,这篇 2004 年[星巴克不用二阶段提交](https://www.enterpriseintegrationpatterns.com/ramblings/18_starbucks.html)的博客揭示了业务数据的本质:异步、弱序列、灵活错误处理(放弃/重试/补偿)、最终一致性。
18 |
19 | 近些年分布式系统的趋势回应了类似我们业务系统需求的特点。大趋势是采用异步、分布式并发的工作方式。事物处理也大都是采用最终一致性的工作方式,放弃强一致性换来高并发和高可靠。
20 |
21 | 考虑到大量的外部调用和内部各个系统之间的交互,回压 Backpressure,熔断机制,超时,重试等机制也是必不可少。
22 |
23 | ## 技术选型
24 |
25 | 综合业务与技术系统需要,我们的业务系统最好就是一种基于工作流(workflow)的工作方式,同时灵活的支持模块组合和异步执行。
26 |
27 | 因为对技术要求比较高,灵活的 Workflow 的工作方式并不多见。[Jet Tech 的 Workflow 博客](https://medium.com/jettech/microservices-to-workflows-expressing-business-flows-using-an-f-dsl-d2e74e6d6d5e) 是一个比较出名的例子。
28 |
29 | 
30 |
31 | Workflow 的一个基本要求就是把数据和处理数据的函数分开。传统的面向对象编程(封装和继承)在这种场合是一种反模式的模型。函数式编程模型,尤其是响应式流处理(reactive streams)和 workflow 有着天然的契合。流行的基于 JVM 的异步流处理框架有 RxJava, Spring Reactor, Vert.x, Ratpack 和 Akka。
32 |
33 | 除 Akka, 其他框架/类库都比较新而且工作在较低的 Reactive Streams 模型上。使用者需要在 `Flux`, `Mono`, `Subscription` 这种原始概念上逐层建立自己的高层次业务抽象模型。RxJava 有比较完善的开源生态系统。可是采用 Java 语言,在错误处理和运行监控方面的处理都比较复杂。一种流行的说法是使用 RxJava 的工程师需要有六年以上 Java 工作经验。
34 |
35 | Akka 就是为了异步分布式处理量身定做的一个工具库(toolkit)。借鉴了 Erlang OTS 的 Actor model, Akka Actor 发布于 2009 年 7 月, Akka Streams 2015 年 4 月发布。做为 Rective Streams 的标准制定参与者和异步分布式处理的长期实践者,Akka Streams 的开发者明白应用开发需要高层次的抽象工具。2014 年 9 月发布的[Akk Stream 预览版](https://akka.io/blog/news/2014/09/12/akka-streams-0.7-released) 就封装了底层接口并支持流程领域特定语言(Flow DSL)来定义灵活组合流程(flow)的 流程图(flow graph)。
36 |
37 | 
38 |
39 | 借鉴 [Jet Workflow](https://medium.com/jettech/microservices-to-workflows-expressing-business-flows-using-an-f-dsl-d2e74e6d6d5e), 一个好的工作流工具库需要满足下面几个条件:
40 |
41 | - 强类型:太多的复杂数据类型,需要编译时验证和编程时即时帮助。
42 | - 可读性:清楚定义流程步骤和分支,容易理解和维护。
43 | - 显式错误处理:支持基本的丢弃、重试和补偿错误处理方式。
44 | - 可扩展:以一种标准方式支持灵活的业务处理组合与扩展。
45 |
46 | 相比微服务,workflow 是更高层次的抽象。如下图:
47 |
48 | 
49 |
50 | 下面是一个 Jet Tech 的流程处理例子。
51 |
52 | 
53 |
54 | Jet Tech 在 2018 年用 FSharp 开发了一套自己的内部工作流工具库。而 Akka Streams 是一个成熟的开源软件,如果不想从头开发,基本上是目前的唯一选择。
55 |
56 | ## 数据库访问
57 |
58 | 目前流行的数据库访问模式有三种:ORM、SQL 和 Type-safe SQL。对应的实现有 Hibernate, JdbcTempalte/MyBatis, 和 jOOQ/QueryDSL。数据库访问应该满足下面条件:
59 |
60 | - 显式的数据库访问:清清楚楚知道连接的范围,事务处理的范围,数据是否缓存等。
61 | - 静态类型&类型安全:可以编译检查数据类型,也方便重构。
62 | - 高效&精准控制:只查询所要的数据和修改要修改的数据,一次数据库访问完成。
63 | - 简单的关联数据查询:方便的访问有关系的表。
64 | - 方便的数据映射:程序数据结构和数据库表数据的方便转换。这个函数式语言有先天优势。
65 | - 支持原始 SQL: 总有场合需要这个功能。
66 | - 手工或自动元数据生成。
67 |
68 | 在 Java 语言里面,只有 jOOQ/QueryDSL 满足上面多数条件。如果采用函数式语言如 Scala,则打开了另一扇门 Functioinal Relational Mapping (FRM)。由于关系型数据库的基本操作(关系运算和关系演算)是函数演算的一个子集,FRM 有着天然的契合度。Scala 的 [Slick FRM 库](https://slick.lightbend.com/) 也满足上面的要求。
69 |
70 | ## 可选技术栈
71 |
72 | Akka 同时支持 Java 和 Scala。考虑到从 Web API 服务到数据库访问的整个流程处理,我们有四种选择。
73 |
74 | - Java: Spring WebFlux + Akka Streams/Actor + jOOQ (方案一)
75 | - Java: Spring DI + Akka HTTP + Akka Streams/Actor + jOOQ (方案二)
76 | - Java: Spring DI + Play framework + Akka Streams/Actor + jOOQ (方案三)
77 | - Scala: Play framework + Akka Stream/Actor + Slick (方案四)
78 | - Scala: Akka HTTP + Akka Stream/Actor + Slick (方案五)
79 |
80 | 前三个方案都是基于 Java 和 Spring DI,主要是为了方便现有团队的技术升级。方案一和方案二都已经开发出了概念原型,包括 jOOQ 的异步访问库。方案三花了一天时间还没有调通。方案四和五是 Scala 的标准配置,没有什么悬念。
81 |
82 | 方案一有二个变种,在 Controller 层返回 Akka Steam 的 `Source` 或 Spring Reactor 的 `Flux/Mono`。前者按官方文档是支持的,可是一直没有调通,虽然都是同一个标准,有双方接口支持,可是其实并不匹配。另一个变种调通了,可是代码非常难看。
83 |
84 | 方案二比较可行灵活。不过需要自己实现 Web Api 层的路由,参数转换/验证,错误处理框架和 pluting 框架,有一些工作量。在整个链条处理上,处处感觉到 Java 的坑,既有 Java 非函数语言本身的特点,也有 Akka 把 Java 做为二等公民的不给力支持。学习资料比较少。
85 |
86 | 方案三官方的库很久没有更新,做起来需要对二个框架都需要比较深的了解,每次版本升级都有坑要填,常见还是会弃用 Spring 框架。
87 |
88 | 方案四是个 Scala 的标配方案,优点非常明显:
89 |
90 | - 功能强大:有我们梦寐以求的工作流处理能力 (Akka Streams)。而且 Stream-based 的异步工作方式是未来趋势,有很高的性能和很强的扩展能力。一旦掌握,开发效率也很高。
91 | - 成熟: 各个技术板块都有 10 年左右的实践经验,是所有方案里面最可靠的。
92 | - 原生支持:Scala 是 Akka 的原生开发语言,文档也很丰富。
93 | - 技术优势明显:相比其其它混合框架和非原生编程语言模式,方案四是自包含生态,没有集成问题。
94 | - Slick 的 FRM 数据库访问是目前最好的模式。技术层面远超其它 ORM 或 typesafe SQL。比起 基于 Scala 的新数据库框架 [quill](https://getquill.io/), Slick 的优点是 Stream API,能够和 Akka Streams 无缝集成。
95 |
96 | Slick FRM 数据库访问的优点:
97 |
98 | - 基于集合的概念,和 SQL 概念吻合
99 | - 强类型
100 | - 手工或自动的模式生成
101 | - 支持复杂的关系查询
102 | - 方便的多语句查询和事物处理
103 | - 支持原生 SQL 和数据转换
104 | - 不用缓存
105 |
106 | 但是方案四的缺点是 Play 过于复杂,很多功能是为了渲染 Web 页面而做,对于我们的 REST API 没有太大用处。方案五和方案四的唯一差别是我们用 Akka HTTP 而不是 Play 做 REST API 的请求处理。Akka HTTP 有我们需要的如下 REST API 服务功能
107 |
108 | - 基于 DSL 的路由配置
109 | - 高层和底层的 HTTP 请求处理
110 | - 方便的 Marshalling and unmarshalling 功能
111 | - 方便的认证和授权的集成
112 | - 较好的错误处理机制
113 | - Stream-based 的接口
114 | - 简单的数据转换处理
115 | - 类型安全
116 |
117 | 方案四和五的最大问题是 Scala 语言。Scala 是一个多范式语言,同时支持面向对象和函数式编程。这成为其最大卖点和最大缺点。短期来看,多范式是一个负资产。具体体现如下:
118 |
119 | - 学习曲线比较陡:二个范式的学习和贯通通常需要三到六个月成为熟手,精通则需要一年以上。
120 | - 有面向对象的经验是学习障碍:函数式的思维很多地方和面向对象是相反的,改掉旧习惯来学习相反的新习惯比没有旧习惯来学习更难。
121 | - 多范式容易让让人走偏:本来函数式是很适合要解决的问题,可是随手可用的面向对象功能随时让人走偏。
122 |
123 | ## 技术栈选择
124 |
125 | 所有的业务运行问题都是人的问题。我们百里挑一组建的团队学习和研发能力是方案选择的最关键考量。
126 |
127 | - 我们团队学习能力强。
128 | - 并行转型方案
129 | - 少数人原型尝试,总结。
130 | - 多数人仍用现有模式开发,六个月到一年完成转型。
131 | - 保底方案:现有 Spring 架构加 jOOQ 替换 Jpa/Hibernatge。
132 |
133 | 结论: 对于 REST API 服务我们采用方案五。如果有网页生成需求,则方案四为最佳选择。特点如下:
134 |
135 | - 基于工作流(workflow)模式开发业务系统。
136 | - 基于流模式的全异步(从 Web 层到数据库)编程。
137 | - 函数式编程: 鼓励不可变数据结构、纯函数、类型和操作的参数化。
138 |
--------------------------------------------------------------------------------
/backend/database/数据库事务与隔离级别.md:
--------------------------------------------------------------------------------
1 | # 数据库事务与隔离级别
2 |
3 | 本文讨论了数据库事物处理的一些基本概念和使用原则。
4 |
5 | ## 事物处理的基本原则
6 |
7 | 看上去纷繁复杂,其实掌握了下面这些基本原则,花时间搞清楚自己在干什么,就完全可以避免数据不一致并保证高并发。
8 |
9 | - 除了缺省的,所有的数据库访问都清晰地写出事物隔离级别。
10 | - 只读取所需要的数据,不读多余的。不要懒加载,早加载所有用到的数据,不加载多余的。
11 | - 只更新改变的数据,不写多余的。处理创建或整体修改,不要整体 save, 只 update 改变的数据.
12 | - 不需要事物,就不要用。比如,如果更新不需要检查条件(比如更改地址),则直接更新,后面的提交覆盖前面的版本。
13 | - 如果更新有一定条件,比如取消订单需要订单的状态是可取消状态,则更新时需要先用 select for update 检查更新的条件,符合条件再更新,不符合条件返回相应的业务错误代码。
14 | - 尽量缩小事物范围,尽可能晚开始事物,尽可能早提交或回滚。
15 | - 并采用正确的事物隔离级别。拿不准就正确性第一,用高一级的事物隔离。
16 | - 尽量缩小占用数据库连接的时间。用的时候拿,用完立刻归还。
17 |
18 | ## 数据库事物处理(Transaction)
19 |
20 | 数据库的数据不一致可以分为二大类:读写不一致和更新丢失。数据库依靠事务处理来保证数据一致性和并发性。乐观锁是一种轻量的事物处理机制,但是使用场合非常有限。
21 |
22 | ### 读写不一致
23 |
24 | 脏读、不可重复读和幻读[三个问题](https://juejin.im/post/5b90cbf4e51d450e84776d27)都是源于事务 A 对数据进行修改时同时有另一个事务 B 在做读操作造成的。
25 |
26 | - 脏读(Dirty reads): 针对未提交数据
27 |
28 | 事务 A 对数据进行了更新,但还没有提交,事务 B 可以读取到事务 A 没有提交的更新结果,这样造成的问题就是,如果事务 A 回滚,那么,事务 B 在此之前所读取的数据就是一笔脏数据。
29 |
30 | - 不可重复读(Non-repeatable reads): 针对其他提交前后,读取数据本身的对比
31 |
32 | 不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务 A 在事务 B 的更新操作之前读取一次数据,在事务 B 的更新操作之后再读取同一笔数据一次,两次结果是不同的。
33 |
34 | - 幻读(Phantom reads): 针对其他提交前后,读取数据条数的对比
35 |
36 | 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。
37 |
38 | | | 更新丢失 | 脏读 | 不可重复读 | 幻读 |
39 | | -------------- | -------- | ---- | ---------- | ---- |
40 | | 读未提交(RU) | 避免 | | | |
41 | | 读提交(RC) | 避免 | 避免 | | |
42 | | 可重复读(RR) | 避免 | 避免 | 避免 | |
43 | | 串行化(S) | 避免 | 避免 | 避免 | 避免 |
44 |
45 | 不可重复读的重点是修改,指的是同样条件读取过的数据,再次读取出来发现值不一样。
46 | 幻读的重点是数据条数的变化(新增或删除),指的是同样的条件,两次读出来的记录数不一样。
47 |
48 | ### 更新丢失
49 |
50 | [更新丢失(Lost updates)](https://blog.csdn.net/u014590757/article/details/79612858)针对并发写数据.多个 session 对数据库同一张表的同一行数据进行修改,时间线上有所重复,可能会出现各种写覆盖的情况。具体说就是后面的写操作会覆盖前面的写操作。
51 |
52 | ### 事务隔离级别
53 |
54 | 为保证数据一致性,数据库支持不同的[事务隔离级别](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html)。数据库事务有四种隔离级别,由低到高分别为:
55 |
56 | - 读未提交(Read uncommitted,RU): 最低的隔离级别,指的是一个事务可以读其他事务未提交的数据。
57 | - 读提交(Read committed,RC): 一个事务要等另一个事务提交后才能读取数据。
58 | - 可重复读(Repeatable Read,RR): 在开始读取数据(事务开启)时,不再允许修改操作。
59 | - 串行化(Serializable,S): 最高的事务隔离级别,事务串行化顺序执行。
60 |
61 | ### 不建议用乐观锁
62 |
63 | 乐观锁就是给一个数据加一个版本,每次写的时候先检查是否开始读出的数据是最新版本,如果是,则更新数据和版本。如果数据在读之后,写之前已经修改有了更新的版本,则报错。乐观锁适用于并发写数据不常见而且可以自动或方便的人工修复的场景。如果并发写经常发生,就不符合乐观的假设了。如果不能自动或方便的人工修复,由于没有事物等待机制,处理事物失败的成本会很高。
64 |
65 | 所以通常不建议用乐观锁。
66 |
67 | ## Spring JPA
68 |
69 | ### 关于 Hibernate Dynamic update
70 |
71 | 使用 ORM save 方法实现数据持久化的情况下,开启 Dynamic update,使得保存更改时影响的字段仅限于被改动了字段。此方案通过控制更新字段的范围,尽量减少脏操作可能,但也无法完全避免。主要缺陷
72 |
73 | - 语义错位。本意是直接修改部分属性,现在变成取整个 Object,改部分属性,存整个 Object。中间不可控因素太多。
74 | - 每次根据改动了的字段,动态生成 SQL 语句,性能上相比全更操作有所降低。
75 | - 需要从数据库拿到整个 Object 所有数据才能修改,大多数时候不必要。
76 | - 当两个 session 同时对同一字段进行更新操作,会出现各种数据一致性错误。示例见:[Stackexchange Q: What's the overhead of updating all columns, even the ones that haven't changed](https://dba.stackexchange.com/questions/176582)。
77 |
78 | ### 如何更新数据库字段
79 |
80 | - 只有在创建新的数据时使用 Spring Data JPA 的 save 方法。
81 | - 不要在更新数据时使用 Spring Data JPA 的 save 方法。
82 | - 默认配置且未使用锁的情况下,save 方法会更新实体类的所有字段,一方面增加了开销,另一方面歪曲了更新特定字段的语义,多线程并发访问更新下的情况下易出现问题。
83 | - 配置动态更新且未使用锁的情况下,save 方法会监测改动了的字段并进行更新,但是由于脏读的可能性,更新的数据可能出错。
84 | - 总的来看,使用 ORM save 方法进行实体类更新陷入了 “You wanted a banana but you got a gorilla holding the banana” 的怪圈,导致做的事情不精确、或者有其它的风险。[参考文章](https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/)
85 | - 使用自定义 SQL 进行字段更新
86 | - 使用 JPA 提供的 @Query/@Modifying 书写 JPQL 进行精确控制的字段更新操作。
87 |
88 | ### 处理 Hibernate 懒加载
89 |
90 | 当一个对象有很多关联的数据时,关联的数据只有在使用时才被加载就叫懒加载。
91 |
92 | 懒加载在我们项目中带来的问题:
93 |
94 | #### 问题一:N + 1 问题
95 |
96 | ##### 描述
97 |
98 | 使用 Spring Data JPA 进行包含列表子对象的对象的列表查询时,若最后使用的结果集不仅限于该对象本身,而还包含其子对象中的内容,会出现 N + 1 问题。
99 |
100 | ##### 解决
101 |
102 | 列表查询改用 Spring Jdbc Template 直接书写原生 SQL 语句执行查询,最大程度上提高效率
103 |
104 | #### 问题二:session closed 问题
105 |
106 | ##### 描述
107 |
108 | 使用 Spring Data JPA 查询数据时,若是从非 Controller 环境(如消息队列消费者等异步线程环境),访问对象下面的列表子对象会出现 session closed 异常。因为此时没有 session,没有懒加载机制。
109 |
110 | ##### 解决
111 |
112 | 1. 设置 Hibernate 属性(v4.1.6 版本后可用):hibernate.enable_lazy_load_no_trans=true
113 | 2. 使用 @Fetch(FetchMode.JOIN) 注解
114 | 3. 使用 @LazyCollection(LazyCollectionOption.FALSE) 注解
115 | 4. Spring boot 的 Open Session In View (OSIV) 思想借鉴,封装了 @OpenJpaSession 注解。
116 |
117 | ### 使用 AspectJ
118 |
119 | 建议 AOP 用 aspectJ:
120 |
121 | ```xml
122 |
123 | ```
124 |
125 | 相较于 Java JDK 代理、Cglib 等,AspectJ 不但 runtime 性能提高一个数量级,而且支持 class,method(public or private) 和同一个类的方法调用。可以把@Transaction 写到最相关的地方。坏处是配置和 build 可能稍稍有些麻烦。
126 |
127 | ## 事务的使用建议
128 |
129 | - 减少外键使用
130 |
131 | 插入操作会需要 S lock 所有的外键。所以像 History 或审计之类的表不要和主要业务表建立外键,可以建个索引用于快速查询就是了,这样也实现了表之间的解耦。
132 |
133 | - 锁的使用
134 |
135 | 尽可能避免表级别的锁。如果很多需要串行处理的操作,可以建立一个辅助的只有一行的 semaphore(信号)表,事物开始时先修改这个表,然后进行其他业务处理。
136 |
137 | - 最佳实践
138 |
139 | 1. 所有查询放在事务之外,多条查询考虑用 readOnly 模式,建议用 READ COMMITTED 事物级别。但是外层事务 readOnly 事务会覆盖内层事务,使内层非只读事务表现出只读特性,我们的处理方式:(待补充)
140 | 2. 远程调用与事务,事物过程里面不许有远程调用。
141 | 3. 在处理中应该先完成一个表的所有操作再处理下一个表的操作。相关的表进行的操作相邻。先业务表再 history/audit 之类的辅助表操作。
142 | 4. 在事物里面处理多个表时,程序各处一定要按照同样的顺序。最好时按照聚合群的概念,从根部的表开始,广度优先,每层指定表的顺序。
143 | 5. 多个表的操作最好封装到一个函数/方法里面。
144 | 6. 序列号生成使用下面的事物模式:
145 |
146 | ```java
147 | @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
148 | ```
149 |
--------------------------------------------------------------------------------
/backend/database/数据库常见问题.md:
--------------------------------------------------------------------------------
1 | # 数据库常见问题
2 |
3 | ## 1 Illegal mix of collations
4 |
5 | 在执行某些 SQL 语句的时候,可能会遇到如下报错:
6 |
7 | ```text
8 | Illegal mix of collations (utf8mb4_unicode_ci,IMPLICIT) and (utf8mb4_general_ci,IMPLICIT) for operation '='
9 | ```
10 |
11 | 这种一般是由于 join 语句两端的分属不同表的字段的 collation 不同造成的。在创建表的时候全部明确指定 utf8mb4_unicode_ci(与 CHARSET = utf8mb4 配套) 为佳:
12 |
13 | ```sql
14 | ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='xx';
15 | ```
16 |
17 | 可以通过如下语句来检查表的 charset 和 collation 相关属性:
18 |
19 | ```sql
20 | -- for table
21 | SELECT TABLE_SCHEMA
22 | , TABLE_NAME
23 | , TABLE_COLLATION
24 | FROM INFORMATION_SCHEMA.TABLES
25 | WHERE TABLE_NAME = 't_name';
26 | -- for column
27 | SELECT TABLE_SCHEMA
28 | , TABLE_NAME
29 | , COLUMN_NAME
30 | , COLLATION_NAME
31 | FROM INFORMATION_SCHEMA.COLUMNS
32 | WHERE TABLE_NAME = 't_name';
33 | ```
34 |
35 | 如果是已经创建的表,可以通过如下语句来修改:
36 |
37 | ```sql
38 | alter table convert to character set utf8mb4 collate utf8mb4_unicode_ci;
39 | ```
40 |
41 | 如果在执行存储过程的时候报此错误而且存储过程有参数,那么这个错误可能是因为参数的 collation 和表中字段的 collation 不同造成的。参数的 collation 是遵循 schema 的 collation,可以使用如下语句更改 schema 的collation,并重建存储过程即可:
42 |
43 | ```sql
44 | ALTER DATABASE DB_Name
45 | DEFAULT CHARACTER SET = utf8mb4
46 | DEFAULT COLLATE=utf8_unicode_ci;
47 | ```
48 |
49 | 可见,在创建 schema/table 的时候,从一而终地指定 character set utf8mb4 collate utf8mb4_unicode_ci 非常重要,不然排查BUG会排到让人怀疑人生。
50 |
--------------------------------------------------------------------------------
/backend/database/数据库设计规范-mysql.md:
--------------------------------------------------------------------------------
1 | # 数据库设计规范-mysql
2 |
3 | ## 0. 原则
4 |
5 | ## 1. 命名
6 |
7 | - 表名,字段名全部小写,单词间以下划线分隔,如: flight_order。
8 | - 主键建议命名为id。
9 | - 编写sql脚本时,关键字大写, 如:
10 |
11 | ```sql
12 | SELECT id, pay_mode FROM flight_order
13 | ```
14 |
15 | ## 2. 主键选择
16 |
17 | ## 3. 数据列设计
18 |
19 | ## 4. 索引
20 |
21 | ## 5. 最佳实践
22 |
23 | - 每个版本的SQL语句要保证能够重复执行。
24 |
--------------------------------------------------------------------------------
/backend/database/数据库连接池.md:
--------------------------------------------------------------------------------
1 | # 数据库连接池
2 |
3 | 几乎所有的商业应用都有大量数据库访问,通常这些应用会采用数据库连接池。理解为什么需要连接池,连接池的实现原理,系统架构和性能目标对于写出正确、高效的程序很有帮助。这些概念可用于系统运行参数的配置,同时对于理解并发和分布式处理也很有帮助。
4 |
5 | 通用一点来说,一个有经验的工程师面对任何问题都会试着回答三个问题:为什么,是什么,怎么做。了解为什么可以明白问题的真正目的,有助于开放思路和避免无用功。是什么则回答问题的本质概念,是正确答案的保障。怎么做则给出可重复的问题解决思路,使得问题总是以正确、高效的方式得到解决。本篇文章按这个思路来解决数据库连接池如何配置的问题。
6 |
7 | ## 1 为什么需要连接池
8 |
9 | 任何数据库的访问都需要首先建立数据库连接。这是一个复杂、缓慢的处理。牵涉到通信建立(包括 TCP 的三次握手)、认证、授权、资源的初始化和分配等一系列任务。而且数据库服务器通常和应用服务器是分开的,所有的操作都是分布式网络请求和处理。[建立数据库连接时间](https://stackoverflow.com/questions/2188611/how-long-does-it-take-to-create-a-new-database-connection-to-sql)通常在 100ms 或更长。而通常小数据的 CRUD 数据库操作是 ms 级或更短,加上网络延迟一般 10 到 50 个 ms 就可以完成多数数据库处理结果。在应用启动时预先建立一些数据库连接,应用程序使用已有的连接可以极大提高响应速度。另外,Web 服务应用当客户很多时,有很多线程,连接数目过多以及频繁创建/删除连接也会影响数据库的性能。
10 |
11 | 总结起来,采用数据库连接有如下好处:
12 |
13 | - 节省了创建数据库连接的时间,通常这个时间大大超过处理数据访问请求的时间。
14 | - 统一管理数据库请求连接,避免了过多连接或频繁创建/删除连接带来的性能问题。
15 | - 监控了数据库连接的运行状态和错误报告,减少了应用服务的这部分代码。
16 | - 可以检查和报告不关闭数据库连接的错误,帮助运维监测数据库访问阻塞和帮助程序员写出正确数据库访问代码。
17 |
18 | ## 2 数据库连接池是什么
19 |
20 | ### 2.1 实现原理
21 |
22 | 如同多数分布式基础构件,连接池的原理比较简单,但是牵涉到数据库,操作系统,编程语言,运维以及应用场景的不同特点,具体实现比较复杂。从数据库诞生就有的广泛需求,半个世纪后还有不断改进提高的余地。
23 |
24 | 原理上,在应用开始时创建一组数据库的连接。也可以动态创建但是复用已有的连接。这些连接被存储到一个共享的资源数据结构,称为连接池。这是典型的生产者-消费者并发模型。每个线程在需要访问数据库时借用(borrow)一个连接,使用完成则释放(release)连接回到连接池供其他线程使用。比较好的线程池构件会有二个参数动态控制线程池的大小:最小数量和最大数量。最小数量指即使负载很轻,也保持一个最小数目的数据库连接以备不时之需。当同时访问数据库的线程数超过最小数量时,则动态创建更多连接。最大数量则是允许的最大数据库连接数量,当最大数目的连接都在使用而有新的线程需要访问数据库时,则新的线程会被阻塞直到有连接被释放回连接池。当负载变低,池里的连接数目超过最小数目而只有低于或等于最小数目的连接被使用时,超过最小数目的连接会被关闭和删除以便节省系统资源。
25 |
26 | 连接池的实际应用中,最担心的问题就是借了不还的这种让其他人无资源可用的人品问题。编码逻辑错误或者释放连接放代码没有放到 `finally` 部分都会导致连接池资源枯竭从而造成系统变慢甚至完全阻塞的情况。这种情况类似于内存泄露,因而也叫连接泄露,是常常发生而且难以发现的问题。因此检测连接泄露并报警是线程池实现的基本需要。
27 |
28 | 连接在被使用时运行在借用它的线程里面,并不是运行在新的线程里面。但是因为每个连接在使用中要实现超时 timeout 机制,官方的 [Java.sql.Connection.setNetworkTimeout API](https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html)的接口定义是 `setNetworkTimeoutExecutor executor, int milliseconds)`。此处需要指定一个线程池来处理超时的错误报告。也就是每一个连接运行数据库访问时,都会有一个后台线程监控响应超时状态。很多连接池实现会使用 Cached Thread Pool 或 Fixed Thread Pool。Chached Thread Pool 没有线程数目限制,动态创建和回收,适合很多动态的短小请求应用。Fixed Thread Pool 则适合比较固定的连接请求。
29 |
30 | 另外,网络故障和具体数据库实现的限制会使得连接池的连接失效。比如,MySQL 允许一个连接,无论状态正常与否,都不能超过 8 个小时的生命。因此,虽然连接在被使用时运行在调用的线程里面,但是连接池的管理通常需要一个或多个后台线程来管理、维护、和检测连接池的连接状态,保证有指定数目的连接可用。
31 |
32 | 可以看的,虽然数据库连接在执行数据库访问使用调用者的线程,但是连接池的实现通常需要二个或更多的线程池做管理和超时处理。当然连接池的具体实现还要考虑很多细节,但是不直接影响应用接口,放在文章结尾再讨论。
33 |
34 | ### 2.2 数据库连接池的系统架构
35 |
36 | 连接池的本质是属于一个操作系统进程(process)的计数信号量(counting sempphore),用于控制可以并行使用数据库连接的线程数量。在 Java SDK 有一个[Semaphore Class](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Semaphore.html)可以用来管理各种有限数量的资源。连接池的核心管理功能是从池中分配一个数据库连接给需要的线程,线程用完后回收连接到池中。由于连接池有限,可以并行进行数据库访问的线程数量最多是连接池的最大尺寸。如果考虑到一个应用线程可能会用到多个数据库连接的可能性,则可以并发访问数据库的线程数目会更少。
37 |
38 | 连接池的使用者是业务应用程序。通常有二种:一种是基于用户/服务请求的 HTTP 服务线程,通常采用线程池。特点是线程数目动态变化很大,数据库的访问模式比较多样,处理时间也有长有短,可能有很大差别。另一种是后台服务,其线程数目比较固定,数据库访问模式和处理时间也比较稳定。
39 |
40 | 连接池只是给业务应用提供已建立的连接,所有的访问请求都通过连接转发到后台数据库服务器。数据库服务器通常也采用线程(PostgreSQL 每个连对应一个进程)池处理所有的访问请求。
41 |
42 | 具体来说,连接池是两个线程池的中间通道。可以看成下面的结构:
43 |
44 | 一个或多个应用服务进程里面(线程池 <-> 数据库连接池) <===============> 一个数据库服务器线程(或进程)池
45 |
46 | 上图中,连接池和应用服务线线程池在同一个进程里面。每个访问数据库的应用服务进程都有自己的线程池和对应的数据库连接池。数据库服务器可能需要处理来自一个或多个服务器的多个应用服务进程内的数据库连接池数据访问请求。
47 |
48 | ## 3 如何配置数据库连接池
49 |
50 | ### 3.1 配置目标
51 |
52 | 当提到数据库连接池的配置,一个常见也是严重的错误是把连接池和线程池的概念混淆了。如上面系统架构所示,数据库连接池并不控制应用端和数据库端的线程池的大小。而且每个数据库连接池的配置只是针对自己所在的应用服务进程,限制的是同一个进程内可以访问数据库的并行线程数目。应用服务进程单独管理自己的线程池,除了数据库访问还有处理其他业务逻辑,并行的线程数目基本取决于服务的负载。当应用服务线程需要访问数据库时,其并发度和阻塞数目才受到连接池尺寸的影响。
53 |
54 | 做为应用服务和数据库的桥梁,连接池参数配置的目标是全局优化。具体的优化目的有四个:尽可能满足应用服务的并发数据库访问,不让数据库服务器过载,能发现用了不还造成的死锁,不浪费系统资源。
55 |
56 | 尽可能满足所有的应用服务并发数据库访问的意思很简单:所有需要访问数据库的线程都可以得到需要的数据库连接。如果一个线程用到多个连接,那么需要的连接数目也会成倍增加。这时,需要的连接池最大尺寸应该是最大的并发数据库访问线程数目乘以每个线程需要的连接数目。
57 |
58 | 不让数据库服务器过载是个全局的考虑。因为可能有多个应用服务器的多个连接池会同时发出请求。按照 PostgreSQL V11 文档[18.4.3. Resource Limits](https://www.postgresql.org/docs/11/kernel-resources.html),每个连接都由一个单独进程来处理。每个进程即使空闲,都会消耗不少诸如内存,信号(semaphore), 文件/网络句柄(handler),队列等各种系统资源。这篇文章[Number Of Database Connections](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections#How_to_Find_the_Optimal_Database_Connection_Pool_Size) 讨论了 PostgreSQL V9.2 的并发连接数目。给出的建议公式是 `((core_count * 2) + effective_spindle_count)`,也就是 CPU 核数的二倍加上硬盘轴数。值得注意的是,这个并发连接数目并非数据库这面的连接池尺寸。实际上 PostgreSQL 内部并没有连接池,只有允许的最大连接数目 [max_connections](https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS),缺省值为 100。MySQL 采用了不同的服务架构,[MySQL Too many connections](https://dev.mysql.com/doc/refman/5.5/en/too-many-connections.html)给出的缺省连接数目为 151。这二个系统从具体实现机理、计算办法和建议数值都有很大差别,做为应用程序员应该有基本的理解。
59 |
60 | 这个[OLTP performance -- Concurrent Mid-tier connections](https://youtu.be/xNDnVOCdvQ0)视频用一个应用服务线程池进行了模拟。应用服务线程池有 9600 个不断访问数据库的线程,当连接池尺寸为 2048 和 1024 时,数据库处于过载状态,有很多数据库的的等待事件,数据库 CPU 利用率高达 95%。当连接池减少到 96,数据库服务器没有等待事件,CPU 利用率 20%,数据库访问请求等待时间从 33ms 降低到 1ms,数据库 SQL 执行时间从 77ms 降低到 2ms。数据库访问整体响应时间从 100ms 降低到 3ms。这时一个应用服务线程池对一个数据库服务线程池的情况,总共 96 个连接池的数据库处理性能远远超过 1000 个连接池的性能。数据库服务器需要为每个连接分配资源。
61 |
62 | 能发现用了不还造成的阻塞也是选择连接池实现的基本需求。应用程序错误会造成借了不还的情况,反复出现会造成连接池用完应用长期等待甚至死锁的状态。需要有连接借用的超时报错机制,而这个超时时间取决于具体应用。
63 |
64 | 不浪费系统资源是指配置过大的连接池会浪费应用服务器的系统资源,包括内存,网络端口,同步信号等。同时线程池的重启和操作都会响应变慢。不过应用端连接池的开销不是很大,资源的浪费通常不是太大问题。
65 |
66 | ### 3.2 配置方法
67 |
68 | 概念清楚,目标明确之后,配置方法就比较容易了。连接池需要考虑二种约束:二端线程(进程)池尺寸约束和应用吞吐量约束。综合考虑二种方法的结果会是比较合理的。
69 |
70 | 二端约束: 找出二端的最大值,其中小的那个值就是连接池上限。应用服务线程池尺寸,比如 Tomcat 最大线程池尺寸缺省值为 200。如果每个线程只用一个数据库连接,那么连接池最大数目应该小于等于 200。如果有些请求用到多于一个连接,则适当增加。如果数据库线程(进程)池的最大尺寸为 151, 取二个值(200, 151)中的那个小的,那么连接池最大尺寸应该小于等于 151。如果还有其他连接池,则还要全局考虑。这个值是连接池的上线。
71 |
72 | 应用负载约束: 考虑应用服务的负载性质。应用服务可以分成二类。一类是数量变化很大的 Web 应用服务线程池,那么连接池也可以配置成动态的,配置相应的最小值和最大值。另一类是像邮件服务这种固定负载的业务应用,可以配置固定尺寸的进程池。这二类应用都可以按照数据库访问的复杂度和响应时间进行估算。这里用到[Little's Law](https://en.wikipedia.org/wiki/Little%27s_law):`并发量 = 每秒请求数量 * 数据库请求响应时间`。注意:这里的请求响应时间包括网络时间+数据库访问时间。很多时候网络时间大于数据库访问时间。如果一个应用线程有多个数据库访问请求,尤其是有事物处理的时候,这个数据库请求响应时间其实是持有连接的时间,公式变为:`并发量(连接数): 每秒请求数 (QPS)* 数据库连接持有时间`。
73 |
74 | 如果每秒有 100 个数据库访问请求,每个数据库访问请求需要 20ms,那么并行量是 `100 * 0.02 = 2`,2 个并发数据库连接就可以了。同理,如果每个请求需要 100ms,那么就需要 10 个并发连接。
75 |
76 | 仅仅考虑二端线程(进程)池的尺寸会配置过大的连接池,因为这是系统的上限。由于数据库访问仅仅是应用线程的一部分工作。 原因是在线程数目计算公式里:`线程数目 = CPU核数 * CPU 利用率 * (1 + 等待时间 / CPU计算时间)`, 数据库的等待时间只是线程所有操作的等待时间的一部分。
77 |
78 | 仅仅配置最小和最大连接数目仅仅是开始,根据具体实现不同,还需要配置连接生命周期,连接超时,未释放连接以及健康监控等其他参数。具体需要参考连接池的使用文档。
79 |
80 | ### 3.3 一个表面相关,其实无关的计算公式
81 |
82 | 因为连接池和线程池经常被混淆,这里有必要介绍另外一个经常提到但是无关的线程数目计算公式。这个公式来自每个 Java 程序员都应该阅读的[Java Concurrency in Practice](http://jcip.net/)。在原著 8.2 节, 第 171 页作者给出了著名的线程数目计算公式:`线程数目 = CPU核数 * CPU 利用率 * (1 + 等待时间 / CPU计算时间)`。这个公式考虑了计算密集(计算时间)和 I/O 密集(等待时间)的不同处理模式。[计算进程的 CPU 使用率](https://stackoverflow.com/questions/1420426/how-to-calculate-the-cpu-usage-of-a-process-by-pid-in-linux-from-c)给出了具体的技术方式和 Script。可是这个公式可以用于应用服务线程池或任何线程池的尺寸估算,但是与数据库连接池的大小估算无关。因为进程池并不能控制应用服务的线程数目,它控制的是可并发的数据库访问线程数目。这些线程使用数据库连接完成网络服务和远程数据库的异步操作,此时基本没有使用本机的 CPU 计算时间。套用公式会得出非常大的数字,没有实际意义。
83 |
84 | ## 4 Spring + MySQL 的应用的连接池配置
85 |
86 | 如上所述,配置 Spring 连接池首先要考虑到其使用的 HTTP 服务的线程池配置和后端数据库服务器的连接数配置。其次是应用的特点。
87 |
88 | ### 4.1 应用服务的线程数
89 |
90 | Spring 的 `server.tomcat.max-threads` 参数给出了最大的并行线程数目,缺省值是 200. 由于采用特殊处理,这些线程可以处理更大的 HTTP 连接数目 `server.tomcat.max-connections`,缺省值是 10000. `spring.task.execution.pool.max-threads`则控制使用`@Async`的最大线程数目, 缺省值没有限制。最好按应用特点配置一个范围。
91 |
92 | ### 4.2 数据库方面的连接数
93 |
94 | MySQL 数据库用`max_connections`环境变量设置最大连接数,缺省值是 151. 多数建议都是根据内存大小或应用负载来设置这个值。
95 |
96 | ### 4.3 基本参数设置
97 |
98 | Spring Boot 缺省使用[HikariCP](https://github.com/brettwooldridge/HikariCP)。
99 |
100 | 需要配置的基本参数如下。
101 |
102 | - maximumPoolSize: 最大的连接数目。超过这个数目,新的数据库访问线程会被阻塞。缺省值是 10。
103 | - minimumIdle: 最小的连接数目。缺省值是最大连接数目。
104 | - leakDetectionThreshold: 未返回连接报警时间。缺省值是 0,不启用。这个值如果大于 0,如果一个连接被使用的时间超过这个值则会日志报警(warn 级别的 log 信息)。考虑到网络负载情况,可以设置为最大数据库请求时长的 3 倍或 5 倍。如果没有这个报警,程序的正确性很难保证。
105 | - maxLifetime:最大的连接生命时间。缺省值是 30 分钟。官方文档建议设置这个值为稍小于数据库的最大连接生命时间。MySQL 的缺省值为 8 小时。可以设置为 7 小时 59 分钟以避免每半个小时重建一次连接。
106 |
107 | ### 4.4 针对数据库的优化设置
108 |
109 | HikariCP [建议的 MySQL 配置](https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration)参数和建议值如下,这些配置有助于提高数据库访问的性能. 注意,这个配置是对数据源(dataSource)配置,不是连接池的配置。这些参数的说明在[MySQL JDBC 文档](https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html)
110 |
111 | - `dataSource.prepStmtCacheSize`: 250-500. Default: 25.
112 | - `dataSource.prepStmtCacheSqlLimit`: 2048. Default: 256.
113 | - `dataSource.cachePrepStmts`: true. Default: false.
114 | - `dataSource.useServerPrepStmts`: true. Default: false.
115 |
116 | ## 5 其他
117 |
118 | ### 5.1 连接池其他实现细节
119 |
120 | 具体的连接池实现需要考虑很多应用细节。
121 |
122 | - 数据库连接的使用还牵涉到事物处理,Spring 的同步数据库访问采用 ThreadLocal 保存事物处理相关状态。所以连接池执行数据库访问时必须在调用者的线程,不能运行在新的线程。Spring 异步数据库访问则可以跨线程。
123 | - 多余的连接不会立即关闭,而是会等待一段空闲时间(idle time)再关闭。
124 | - 连接有最长生命时间限制,即使连接池不管,数据库也会自动关闭超过生命时间的连接。在 MySql 里面,连接最长生命时间是 8 个小时。连接池需要定期监控清理无效的连接。
125 | - 连接池需要定期检查数据库的可用状态甚至响应时间,及时报告健康状态。[HikariCP Dropwizard HealthChecks](https://github.com/brettwooldridge/HikariCP/wiki/Dropwizard-HealthChecks)是一个例子。
126 | - 当需要为新线程访问创建连接时,新线程应该等待池里第一个可用的连接而不必等待因它而创建的线程。HikariCP 的文档[Welcome to the Jungle](https://github.com/brettwooldridge/HikariCP/blob/dev/documents/Welcome-To-The-Jungle.md) 描述了这种实现的优点:可以避免创建很多不必要的连接并且有更好的性能。Hikari 用 5 个连接处理了 50 个突发的数据库短时访问请求,即提高了响应速度,也避免了创建额外的连接。
127 | - 数据库各种异常的处理。[Bad Behavior: Handling Database Down](https://github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down) 给出里不同连接池构件实现对于线程阻塞 timeout 的不同处理方式。很多连接池构件不能正确处理。
128 | - 线程池的性能监视。[HikariCP Dropwizard Metrics](https://github.com/brettwooldridge/HikariCP/wiki/Dropwizard-Metrics) 给出了监视的性能指标。
129 | - 线程阻塞的机制以及相关数据结构对连接池的性能有很大影响。[Down the Rabbit Hole](https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole)给出了 Java 里的优化方法。坏处是里面有些优化过于琐碎,使得代码晦涩难懂而且需要额外维护工作。
130 |
131 | ### 5.2 一些参考缺省配置
132 |
133 | [HikariCP](https://github.com/brettwooldridge/HikariCP): DEFAULT_POOL_SIZE = 10
134 |
135 | [DBCP](https://wiki.apache.org/commons/DBCP): Max pool size : 8
136 |
137 | [c3p0](https://github.com/swaldman/c3p0): MIN_POOL_SIZE = 3, MAX_POOL_SIZE = 15
138 |
139 | [JIRA Tuning database connections](https://confluence.atlassian.com/adminjiraserver070/tuning-database-connections-749382655.html):pool-max-size = 20. 和前三个不同,这是一个数据库应用程序。里面讨论了数据库的连接数目,提到一方面数据库可以支持数百并行连接,另一方面应用服务端的连接还是比较耗费资源,建议在允许的情况下尽可能设成小的数字。
140 |
141 | ### 5.3 题外话
142 |
143 | 网上搜了很多,没有想到这么简单的一个数据库连接池配置问题竟然没有比较全面、明确的文档。把连接池和线程池搞混的的人很多。甚至实施 HikariCP 的程序员在初始化连接池的时候使用了错误的线程池数目。创建线程池的开销主要是网络和远程数据库服务请求的延迟,几乎不耗费 CPU 资源。按照线程计算公式,此时线程池可以很大。可是 HikariCP 的程序员还是仅仅用了`Runtime.getRuntime().availableProcessors()`数目的线程用于创建连接池。正确的数目应该是配置的最小连接池数目,这样既不浪费(在连接数小于 CPU 核数时),也有最好的性能(在连接数超过 CPU 核数时)。参考这个 Issue:[Change the thread pool size to minimumIdle on blocked initialization](https://github.com/brettwooldridge/HikariCP/issues/1375)。 这种错误并不奇怪,因为 HikariCP 的代码风格比较糟糕。很多广泛使用的开源软件其实代码质量并不高,每个人都应该搞清楚概念和问题的本质,多理解其他人的想法,但是保持怀疑态度和独立思考能力。
144 |
--------------------------------------------------------------------------------
/backend/database/是否要用redis.md:
--------------------------------------------------------------------------------
1 | # 是否要用 Redis
2 |
3 | 团队采用 Redis 缓存了用户 Session 和一些其他基础数据和一些查询结果。这是个大的系统结构设计,需要仔细权衡收益和成本。
4 |
5 | ## 问题
6 |
7 | 考虑到在 MySQL 之外再采购部署一套 Redis 会产生如下的问题:
8 |
9 | - 额外的费用:相同的内存配置,和 MySQL 数据库服务价钱差不多。
10 | - 多了一个系统失效节点:本来只有 MySQL,现在如果 Redis 失效,则整个业务系统也无法运行。
11 | - 额外的 API:需要学习使用多一套 API。
12 | - 数据同步难题:如果将 MySQL 查询结果放到 Redis,数据的同步是个难题。
13 | - 另外的部署:需要增加一套账户和配置参数。
14 |
15 | ## 答案
16 |
17 | 具体到我们的具体应用,上述问题的答案如下:
18 |
19 | - 额外的费用:即使和数据库费用差不多,但是在 MySQL 实现那些功能更花钱而且不稳定。
20 | - 多了一个系统失效节点:阿里云有分布式可靠措施,Redis 也比较成熟,可以信赖。
21 | - 额外的 API:团队使用 Redis 的程序员普遍认为其 API 简单易用。而且类似 Hash, Set, List 这样的数据结构在我们应用里有许多应用场景。
22 | - 数据同步难题:尽量避免 MySQL 和 Redis 需要同步的使用。
23 | - 另外的部署:这是一次性的开支,可以基本忽略。
24 |
25 | ## 结论
26 |
27 | 在我们的应用里面有许多地方 Redis 带来很大的方便,尤其是很多只读基础数据和不需要长期保存的数据(比如 Session Token, 短信验证码等), Redis 的 API 非常简单好用而且性能高,系统稳定性非常好。相同的功能如果自己实现成本更高而且稳定性不好。
28 |
29 | 所以可以把 Redis 做为我们的一个基础服务设施使用。根据使用场景,可以把使用中的陷阱和最佳实践写到文档。
30 |
--------------------------------------------------------------------------------
/backend/java/Java后台服务分层规范.md:
--------------------------------------------------------------------------------
1 | # Java 后台服务分层规范
2 |
3 | ## 0. 分层概述
4 |
5 | 好的层次划分可以使代码结构清楚,项目分工明确,可读性大大提升,更加有利于后期的维护和升级。
6 | 本规范借鉴了领域模型的分层方式,其中领域层是整个系统的核心层。
7 |
8 | ## 1. 分层示例:
9 |
10 | 
11 |
12 | 其中各层的引用关系从上到下依次为:
13 | API 层 -> Application 层 -> DomainService -> Repository -> DomainModel
14 |
15 | 引用关系见下图
16 | 
17 |
18 | ## 2. API 层
19 |
20 | API 层用来定义对外的接口,对应于 SpringMVC 的 Controller。
21 | 在 API 层定义接口格式(比如 Swagger 的各种标记),也可以进行参数验证(比如使用@Validated)。
22 | 在 API 层实现登录及权限验证。
23 | API 层应该是很薄的一层,仅仅是参数验证,然后转发到 Application 层。
24 |
25 | ## 3. 应用层(Application 层)
26 |
27 | 应用层的每个方法定义软件要完成的一项任务;
28 | 应用层不包含具体的业务逻辑处理,而是协调业务逻辑层的类来完成任务;
29 | API 返回代码应该在应用层进行定义。
30 |
31 | ## 4. 领域层(Domain 层)
32 |
33 | 领域层负责表达业务逻辑,是整个系统的核心层。
34 | 领域层的类根据职责不同,又划分为以下几部分:DomainModel, DomainService, Repository, ServiceProxy, Bo。
35 |
36 | ### 4.1 领域模型(DomainModel)
37 |
38 | - 领域模型应该是充血模型, 对 DomainModel 状态的修改应通过调用 DomainModel 的方法进行。
39 | - 领域模型参照 DDD 中的聚合,实体,值对象的概念进行划分,对 DomainModel 的任何修改都应该通过聚合根进行。
40 | - 领域模型的创建属于领域层的职责,应该在领域层添加相应的工厂类(对于简单的实体,也可以直接在类中定义一个静态的 create 方法)。
41 | - 领域模型中聚合根应该由自身来维护状态的完整性。
42 | - 领域模型不应该暴露在应用层之外,应该通过 Bo(领域层的数据传输对象)来与领域层外部交互。
43 |
44 | ### 4.2 领域服务(DomainService)
45 |
46 | - 一个领域服务应该只有一个公有方法,用来完成一个独立的业务操作。
47 | - 领域服务用来协调一个或多个聚合,对聚合的更改应委托聚合自身来完成。
48 | - 对于复杂的业务,领域服务允许包含另外的领域服务。
49 |
50 | ### 4.3 仓储(Repository)
51 |
52 | 仓储根据职责分成两类:修改和查询(create,update 和 delete 都归类为修改)。
53 | 这两类仓储可以采用不同的实现方式,比如修改采用 Spring data JPA(Hibernate),而查询可以直接采用 JdbcTemplate。
54 |
55 | ### 4.4 外部服务代理(ServiceProxy)
56 |
57 | 对外部服务的调用应封装在单独的类中。也可以封装出外部服务的接口,调用者只注入接口即可,外部服务的实现类在运行时由 Spring 注入。
58 |
59 | ### 4.5 Bo(领域层的数据传输对象)
60 |
61 | 对于领域层操作所需要的参数,或者领域层返回外部的数据,应该定义 Bo 对象,以避免暴露领域模型到外部。
62 | Bo 对象只应该包含简单的数据字段和一些验证方法,也可以包含从领域模型到 Bo 对象的转换方法。
63 |
64 | ## 5. 基础设施层(infrastructure 层)
65 |
66 | 基础设施层包含一些通用逻辑,如系统配置,常量定义,日志处理,缓存等,以及一些第三方组件的封装。
67 |
68 | ## 6. utility 层
69 |
70 | utility 层包含一些共用的辅助方法。
71 |
72 | ## 7. 最佳实践
73 |
74 | - 对于比较复杂的项目,每个分层中可以根据模块再分成多个目录,以方便管理。
75 | - 事务应该在 ApplicationService 或 DomainService 中定义,具体位置根据情况决定。
76 | - 事务范围应尽量短,事务中不应包括远程服务调用及消息收发操作。
77 |
78 | - 关于消息的发送应遵守以下约定:
79 |
80 | - 消息发送应该封装成独立的类,类名类似于 xxxMessageProducer, 类定义放在 DomainService 目录下。
81 | - xxxMessageProducer 类似 DomainService,可以由其他的 DomainService 或 ApplicationService 调用。
82 |
83 | - 关于消息接收应遵守以下约定:
84 | - 消息接收的具体实现类应定义在最外层中,和 Controller 类似,并创建一个独立的目录,目录名为 consumer,命名类似于 xxxMessageConsumer;
85 | - xxxMessageConsumer 调用 ApplicationService 来完成实际处理。
86 | - xxxMessageConsumer 类似 xxxController,都是由外部调用发起。
87 |
--------------------------------------------------------------------------------
/backend/java/Java命名规范.md:
--------------------------------------------------------------------------------
1 | # Java 命名规范
2 |
3 | ## 0. 原则
4 |
5 | - 命名作为软件中最困难的两个问题之一,需要认真对待;
6 | - 命名有不同的风格习惯,保持团队的命名风格统一有利于大家聚焦在更有价值的事情上;
7 | - 函数和变量命名要有意义,容易理解;
8 | - 良好的命名可以提高软件的可读性,从而易于更于理解和维护。
9 |
10 | ## 1. 项目名
11 |
12 | - 全部小写,单词间以中划线分隔,如: staff-service, config-service。
13 |
14 | ## 2. 包名
15 |
16 | - 全部小写,用名词,如: com.tehang.tmc.services.domain.flight。
17 |
18 | ## 3. 类名和接口名
19 |
20 | - 类名遵守 Pascal 命名法(即首字母大写,多个单词组成时,每个单词的首字母大写)。如:
21 |
22 | ```java
23 | public class FlightOrder {}
24 | ```
25 |
26 | - 接口的命名和类保持一致
27 |
28 | ## 4. 方法名
29 |
30 | - 方法名遵守 camel 命名法(即首字母小写,多个单词组成时,从第二个单词开始,每个单词的首字母大写。
31 | - 方法名应为动词或动词词组,如:
32 |
33 | ```java
34 | public void cancelOrder()
35 | ```
36 |
37 | ## 5. 变量名
38 |
39 | ### 5.1 普通变量名
40 |
41 | - 采用 camel 命名法,一般为名词形式,如:
42 |
43 | ```java
44 | FlightOrder order = null;
45 | ```
46 |
47 | - 禁止使用 i, j 等单字母变量,应使用更有意义的名字;
48 | - 代码中一般不允许直接写入数字(magic number)和字符串,应定义成更有意义的常量名称后使用;
49 |
50 | - **例外**:对于 0 和 1, 在某些情况下,直接写入数字可能更易于理解,如下:
51 |
52 | ```java
53 | /**
54 | * 获取订单的第一个乘机人,这种情况下,直接使用0比常量更易于理解,是可以接受的
55 | */
56 | FlightOrderPassenger passenger = order.getPassengers.get(0);
57 | ```
58 |
59 | ### 5.2 常量名
60 |
61 | - 全部大写,单词间以下划线分隔, 如:
62 |
63 | ```java
64 | public static final String ORDER_STATUS = "TicketConfirmed";
65 | ```
66 |
67 | ## 6. 关于缩写
68 |
69 | - 一般不允许使用缩写,除非该缩写是大家公认的,没有异议的,比如 id,dto 等
70 | - 引入新的缩写词,需经团队成员共同确认,并列在下表中
71 |
72 | - id: 实体类的主键字段
73 | - dto: 数据传输对象
74 | - bo: 业务层使用的数据传输对象
75 | - repo:数据访问层使用的 JPA 的 Repository 类型的变量,以 xxxRepo 命名
76 | - utils: 辅助性质的工具类,以 xxxUtils 命名
77 | - spec: 单元测试的类名, 以 xxxSpec 命名,其中 xxx 表示待测试的原始类名
78 |
79 | - 在变量、类命名时,统一采用一下缩写方式:
80 | - Repository --> Repository
81 | - DomainService --> Service
82 | - ApplicationService --> Application
83 |
--------------------------------------------------------------------------------
/backend/java/Log中的trace记录.md:
--------------------------------------------------------------------------------
1 | # 系统 Log 内的 trace 记录
2 |
3 | 系统中采用了 sleuth+zipkin 进行日志的 trace 记录,在 trace 记录中大致会遇到以下两种情况:
4 |
5 | ## 从 API 层面进来的请求
6 |
7 | 此时请求内部已经生成了 traceID,spanID 等需要的信息,不许做额外处理,仅需正常记录日志即可。
8 |
9 | ## 非外部请求,内部定时器等自启动线程/任务的 trace 记录
10 |
11 | 这类线程/任务的启动由系统内部自定控制,在执行前,并没有生成相应的 traceID 等,此时需要手动生成以下:
12 |
13 | ```java
14 | // 需注入此对象
15 | private Tracer tracer;
16 |
17 | public void execute() {
18 | // 开启一个新的span
19 | ScopedSpan span = tracer.startScopedSpan("newSpanName");
20 | try {
21 | LOG.debug("start *** task");
22 | // do the work you need
23 | } finally {
24 | // 每次任务结束,请及时调用finish方法,否则会一直在一个span内
25 | span.finish();
26 | }
27 | }
28 | ```
29 |
--------------------------------------------------------------------------------
/backend/java/SpringBoot配置.md:
--------------------------------------------------------------------------------
1 | # Spring Boot 配置
2 |
3 | ## 展示/隐藏 Hibernate 生成的 SQL
4 |
5 | 此日志输出无法通过 logging.level.xx 来进行控制,需要在 `application.yml` 中配置如下项为 true(展示)或 false(隐藏):
6 |
7 | - spring.jpa.show_sql
8 |
--------------------------------------------------------------------------------
/backend/java/how-to-handle-exception.md:
--------------------------------------------------------------------------------
1 | # 服务当中,如何处理异常
2 |
3 | - 不允许把产生的异常直接抛出到前端,必须转换成前端可处理的信息抛出
4 |
5 | - 如果是 cache 底层异常,然后再此转换成 applicationException 再抛出的话,统一用下面这种方式:
6 |
7 | ```java
8 | catch (***Exception ex) {
9 | // 将原有异常的信息一并抛出
10 | throw new ApplicationException(ORDER_NOT_EXIST_CODE, ex.getMessage(), ex);
11 | }
12 | ```
13 |
14 | - 如果是在 application 出现错误,这时候需定义清楚 code 和 message,如:
15 |
16 | ```java
17 | if (***true***) {
18 | throw new ApplicationException(FEE_EXCEED_TOTAL_CODE, FEE_EXCEED_TOTAL_MSG);
19 | }
20 | ```
21 |
22 | - domain 层抛出 System Error,application 不需要捕获。
23 |
--------------------------------------------------------------------------------
/backend/java/java-best-practices.md:
--------------------------------------------------------------------------------
1 | # Java 代码最佳实践
2 |
3 | ## try with resource
4 |
5 | 对于外部资源(文件、数据库连接、网络连接等),必须要在使用完毕后手动关闭它们,否则就会导致外部资源泄露。
6 |
7 | Java 7 之前关闭资源的代码很丑陋,应该尽量使用 Java 7 和 Java 9 带来的新语法。当有多个资源时,各个资源用`;`分开,放在 try 后面的括号里面。但是,不是所有资源都能这么写,一定要实现了 AutoCloseable 接口才行。
8 |
9 | ```java
10 | // use the folloiwng in Java 7 and Java 8
11 | try (InputStream stream = new MyInputStream(...)){
12 | // ... use stream
13 | } catch(IOException e) {
14 | // handle exception
15 | }
16 |
17 | // use the following in Java 9 and later
18 | InputStream stream = new MyInputStream(...)
19 | try (stream) {
20 | // ... use stream
21 | } catch(IOException e) {
22 | // handle exception
23 | }
24 |
25 | // NOT the following
26 | InputStream stream = new MyInputStream(...);
27 | try {
28 | // ... use stream
29 | } catch(IOException e) {
30 | // handle exception
31 | } finally {
32 | try {
33 | if(stream != null) {
34 | stream.close();
35 | }
36 | } catch(IOException e) {
37 | // handle yet another possible exception
38 | }
39 | }
40 | ```
41 |
42 | ## 时区概念
43 |
44 | 程序中对时间处理,是根据服务器本地时间来的,所以对时间处理(转换,比较),必须要有时区的概念
45 |
46 | 反例:
47 |
48 | ```java
49 | public static boolean isDateTimeGreaterThanNowOfBeijing(String dateTimeStr) {
50 | DateTime dateTime = DateTime.parse(dateTimeStr, DATE_TIME_PATTERN); // 转换时未指定时区,下面的比对会错误
51 | DateTime now = DateTime.now(DateTimeZone.forID(ZONE_SHANGHAI));
52 | return dateTime.getMillis() > now.getMillis();
53 | }
54 | ```
55 |
56 | 正例:
57 |
58 | ```java
59 | public static DateTime getCstNow() {
60 | return new DateTime(DateTimeZone.forID(ZONE_SHANGHAI)); // 指定时区
61 | }
62 | ```
63 |
64 | ## 接口对外数据类型
65 |
66 | 在返回给客户端的接口中,有些数据类型需要特殊处理:
67 |
68 | 1. double/Double -> String:防止出现 double 转 string 时把不必要的数字也带上
69 | 2. float/Float -> String:防止出现 float 转 string 时把不必要的数字也带上
70 | 3. BigDecimal -> String:BigDecimal 一般用于表示金额,这个需要严肃处理,指定具体的格式化形式,防止默认的转换与预期的要求不符
71 | 4. DateTime/其他时间类型 -> String:时间的格式各异,必须要转为 String 返回
72 |
73 | ## 外部数据的校验
74 |
75 | 对于外部(数据库、接口等)返回的数据,一定要做严格的非空校验,来避免 NPE。
76 |
77 | ## object 内部对属性赋值
78 |
79 | 在 object 内部对属性赋值,使用以下顺序的语法
80 |
81 | 使用当前对象:
82 |
83 | ```java
84 | xxx = XXX
85 | this.xxx = XXX
86 | setXXX(XXX)
87 | this.setXXX(XXX)
88 | ```
89 |
90 | 本对象内新建的对象:
91 |
92 | ```java
93 | object.xxx = XXX
94 | object.setXXX(XXX)
95 | ```
96 |
--------------------------------------------------------------------------------
/backend/java/java中stream使用规范.md:
--------------------------------------------------------------------------------
1 | # java 中 stream 使用规范
2 |
3 | ## stream 使用规范
4 |
5 | ### 1. stream 中的 filter 表达式不要写得太长,对于复杂的表达式建议封装方法
6 |
7 | 例如:
8 |
9 | ```java
10 | List orders = orders.stream()
11 | .filter(order -> StringUtils.equals(order.status, "Submitted")
12 | && StringUtils.equals(order.paymentStatus, "Billed")
13 | && StringUtils.equals(order.authorizeStatus, "Passed"))
14 | .collect(Collectors.toList();
15 | ```
16 |
17 | 可以重构为
18 |
19 | ```java
20 | List orders = orders.stream()
21 | .filter(this::orderCanTicketing)
22 | .collect(Collectors.toList();
23 | ```
24 |
25 | ### 2. 不要嵌套使用 stream,嵌套的 steam 可读性很差,建议将内层的 stream 封装成独立的方法
26 |
27 | ### 3. stream 要适当地换行,不要写在一行中
28 |
29 | ### 4. 不要在 stream 中访问数据库
30 |
31 | 原因: 在循环中访问数据库往往导致性能问题。
32 |
33 | ### 5. 不要使用 stream 来更新数据,只用 stream 来查询
34 |
35 | 例如:
36 |
37 | ```java
38 | List orders = orders.stream()
39 | .filter(this::orderCanTicketing)
40 | .map(this::setTicketSuccess)
41 | .collect(Collectors.toList();
42 |
43 | private FlightOrder setTicketSuccess(FlightOrder order) {
44 | order.setStatus("TicketSuccess");
45 | return order;
46 | }
47 | ```
48 |
49 | 可以重构为
50 |
51 | ```java
52 | List orders = orders.stream()
53 | .filter(this::orderCanTicketing)
54 | .collect(Collectors.toList();
55 |
56 | orders.foreach(this::setTicketSuccess);
57 |
58 | private void setTicketSuccess(FlightOrder order) {
59 | //...
60 | }
61 | ```
62 |
63 | 其本质是,函数式编程不要有副作用。
64 |
--------------------------------------------------------------------------------
/backend/java/java异步编程规范.md:
--------------------------------------------------------------------------------
1 | # java 异步编程规范
2 |
3 | ## Async 使用规范
4 |
5 | 在 java 中推荐的异步编程方式是使用 Spring 的 Async 注解,该方式的优点是简单易用,当方法标注了 Async 注解以后,将在异步线程中执行该方法,但有以下限制:
6 |
7 | - Async 方法必须是类的第一个被调用的方法
8 |
9 | - Async 必须是实例方法,且该方法的对象必须是 Spring 注入的
10 |
11 | 使用时除以上限制需要注意之外,我们还需要解决一些其他问题,如下:
12 |
13 | ### 1. 日志记录
14 |
15 | 由于 Sync 方法并非通过 Controller 进入,绕开了我们的通用异常拦截层,即 CommonExceptionHandler,所以对于异步方法发生的异常没有进行日志记录。
16 |
17 | 为解决此问题,我们编写了**AsyncExceptionLogger**注解,在所有@Async 方法加上此注解,即可保证异常得到记录。
18 |
19 | 例如:
20 |
21 | ```java
22 | @Async
23 | @AsyncExceptionLogger
24 | public void asyncMethod() {
25 | //...
26 | }
27 | ```
28 |
29 | ### 2. 与 JPA 整合
30 |
31 | 由于 Async 方法在新的异步线程中执行,JPA 的 OpenSessionInView 无效,导致执行上下文中并不存在对应的 EnitityManager,这会产生一系列问题,典型场景就是导致 lazyLoading 无效。
32 |
33 | 为解决此问题,我们编写了**OpenJpaSession**注解,在需要访问数据库的@Async 方法中加此注解,即可实现与 OpenSessionInView 类似的效果。但是,方法的入参不能传数据库实体,即不要将数据库实体从一个 session 传到另一个 session,这样是无法获取到该实体的上下文的,这种场景应该传 id,重新查出数据库实体。
34 |
35 | - 示例代码 1:
36 |
37 | ```java
38 | @Async
39 | @OpenJpaSession
40 | @AsyncExceptionLogger
41 | public void asyncMethod() {
42 | //...
43 | }
44 | ```
45 |
46 | - 示例代码 2:
47 |
48 | ```java
49 | @Async
50 | @OpenJpaSession
51 | @AsyncExceptionLogger
52 | public void asyncMethod(long flightOrderId) {// 禁止直接传FlightOrder数据库实体
53 | FlightOrder order = flightOrderRepo.findByIdEnsured(flightOrderId);
54 | //...
55 | }
56 | ```
57 |
58 | ### 3. 使用事务
59 |
60 | 我们当前使用事务的方式是使用@Transactional 注解,但由于@Transational 只能用在对象被调用的的第一个公有方法上,使用起来多有不便(为了事务而创建一个类实在不能接受),而且在很多情况下,我们都需要更加灵活地进行事务范围的控制。
61 |
62 | 为此,我们编写了一个辅助类**TransactionHelper**,使用方式如下:
63 |
64 | ```java
65 | //事务辅助对象,需要注入
66 | private TransactionHelper transactionHelper;
67 |
68 | @Async
69 | @OpenJpaSession
70 | @AsyncExceptionLogger
71 | public void createTicketConfirmTaskIfRequired(long orderId) {
72 |
73 | //创建出票任务,在事务中执行
74 | Long newTaskId = transactionHelper.withTransaction(() ->
75 | doCreateTicketConfirmTaskIfRequired(orderId)
76 | );
77 |
78 | if (newTaskId != null) {
79 | LOG.info("出票任务已创建,开始异步调用出票请求");
80 | ticketingDomainService.autoTicketingAsync(newTaskId);
81 | }
82 | }
83 |
84 | //需要在事务中执行
85 | private Long doCreateTicketConfirmTaskIfRequired(long orderId) {
86 | //todo
87 | }
88 | ```
89 |
90 | 我们只需要将事务中的代码以 lamda 调用的方式包裹在一个 withTransaction 调用中即可。
91 |
92 | ### 4. 线程同步
93 |
94 | 有些场景中,我们需要在主线程中的事务尚未完成的情况下发起一个异步方法调用,在异步方法执行时,又希望在主线程的事务成功提交以后再开始。
95 |
96 | 例如, 我们需要在机票的所有子订单都出票成功的情况下,才开始发送短信通知,代码如下(以下代码仅用来演示):
97 |
98 | ```java
99 | //事务辅助对象,需要注入
100 | private TransactionHelper transactionHelper;
101 | //短信通知服务
102 | private SmsNotifyService notifyService;
103 | //订单仓储
104 | private FlightOrderRepository orderRepo;
105 |
106 | //主线程的出票确认操作
107 | public void ticketConfirm(FlightOrder order, FlightTask ticketConfirmTask) {
108 |
109 | //定义一个异步通知事件,用来在主事务提交完成以后通知异步线程开始
110 | AsyncEvent asyncEvent = new AsyncEvent();
111 |
112 | try {
113 | transactionHelper.withTransaction(() ->
114 | //对每个子订单进行出票操作
115 | order.getOrderTickets().foreach(ticket -> {
116 | //对子订单作出票处理
117 | ticket.ticketSuccess();
118 |
119 | //异步发送通知
120 | notifyService.sendNotifyAsync(order, ticket, asyncEvent);
121 | });
122 |
123 | //其他操作:更新任务已完成
124 | ticketConfirmTask.updateToCompleted();
125 | orderRepo.save(order);
126 | );
127 |
128 | //事务执行成功,通知异步线程开始
129 | asyncEvent.setAsCompleted(true);
130 |
131 | } catch (Excepton ex) {
132 | //事务执行失败,也通知异步线程
133 | asyncEvent.setAsCompleted(false);
134 | thrown ex;
135 | }
136 | }
137 |
138 | //SmsNotifyService类
139 | @Async
140 | @OpenJpaSession
141 | @AsyncExceptionLogger
142 | private void sendNotifyAsync(long orderId, long ticketId, AsyncEvent asyncEvent) {
143 |
144 | //等待主线程执行成功再开始
145 | if (asyncEvent.await() && asyncEvent.isSuccessful()) {
146 | //发送短信通知
147 | //todo
148 | }
149 | }
150 | ```
151 |
152 | 为实现线程间的同步,我们设计了一个 AsyncEvent 类,该类内部包含一个 CountDownLatch 对象,初值为 1。异步线程等待这个 CountDownLatch 对象,当主线程事务完成以后,调用 CountDownLatch 的 countDown()方法,从而触发异步线程的执行。
153 |
154 | AsyncEvent 的代码如下(只列出了主要实现细节):
155 |
156 | ```java
157 | public class AsyncEvent {
158 |
159 | private boolean successful;
160 | private CountDownLatch countDownLatch;
161 |
162 | public AsyncEvent() {
163 | successful = false;
164 | countDownLatch = new CountDownLatch(1);
165 | }
166 |
167 | /**
168 | * 异步线程调用此方法以等待主线程执行完成
169 | * @param timeout 等待时长
170 | * @param unit
171 | * @return 如果主线程在等待时间内执行完成为true, 如果等待超时则为false
172 | */
173 | public boolean await(long timeout, TimeUnit unit) {
174 | try {
175 | return countDownLatch.await(timeout, unit);
176 |
177 | } catch (InterruptedException ex) {
178 | throw new BusinessException(String.format("AsyncEvent await exception happens, message: %s", ex.getMessage()), ex);
179 | }
180 | }
181 |
182 | /**
183 | * 设置主线程执行已完成,调用此方法将触发异步线程开始执行。此方法只能调用一次。
184 | * @param successful 执行成功为true, 否则为false
185 | */
186 | public void setAsCompleted(boolean successful) {
187 | if (this.countDownLatch.getCount() > 0) {
188 | this.successful = successful;
189 | this.countDownLatch.countDown();
190 |
191 | } else {
192 | throw new BusinessException("AsyncEvent.setAsCompleted invoke allowed only once");
193 | }
194 | }
195 |
196 | public boolean isSuccessful() {
197 | return successful;
198 | }
199 | }
200 | ```
201 |
--------------------------------------------------------------------------------
/backend/java/java数据访问层最佳实践.md:
--------------------------------------------------------------------------------
1 | # Java 数据库访问最佳实践
2 |
3 | 团队项目主要使用 Spring Data JPA (以下简称 JPA) 和 Spring JdbcTemplate (以下简称 JdbcTemplate) 进行数据库访问。对这两种工具的选择、使用及特性做如下总结。
4 |
5 | ## 1 根据场景选择工具
6 |
7 | - 对数据的增,删,改主要使用 JPA(底层基于 hibernate 的实现),简单的查询也使用 JPA;
8 |
9 | - 优点:简单,可读性好
10 |
11 | - 如果遇到一定需要手写 update 语句的场景,使用@Query(value = "update ...", nativeQuery = true),再配上@Modifying 和@Transactional
12 |
13 | - 优点:执行修改语句更灵活,想更新什么字段就更新什么字段
14 |
15 | - 对于复杂的查询,比如条件比较复杂,或者用嵌套的子查询,分页等,使用 JdbcTemplate;
16 |
17 | - 优点:使用原生 sql 语句,更灵活
18 |
19 | ## 2 规避 JPA 的常见问题
20 |
21 | 下面的几个问题互相依赖,归根结底,是 [Spring Boot 默认开启 OSIV](https://www.baeldung.com/spring-open-session-in-view) 带来的。此行为的的理由有:
22 |
23 | - 减少 LazyInitializationException,让代码编写更符合直觉,无需关心数据库访问细节
24 | - 傻瓜化数据库访问,数据库连接的概念基本对用户不可见,无需关心连接的获取和释放
25 | - 简单化编码,无需获取 EntityManager 对象即可进行数据库访问,无需显式获取和释放连接
26 |
27 | 在关闭 OSIV 后,需要解决很多的 LazyInitializationException 问题,如果项目已经深陷 OSIV 泥潭,至少先心里有个数,明白这些问题的来龙去脉。
28 |
29 | ### 2.1 N+1 问题
30 |
31 | 该问题为 ORM 的常见问题。在开启 OSIV 的情况下容易静默地出现。考虑有如下数据库表对应的 Java 代码中的实体类数据结构:
32 |
33 | ```java
34 | @Entity
35 | @Table(name = "corp_employee")
36 | public class Employee {
37 |
38 | @Id
39 | private long id;
40 |
41 | /**
42 | * 员工常用联系人,每个员工可有多个联系人,为一对多的关系
43 | */
44 | @OneToMany(
45 | cascade = CascadeType.ALL,
46 | orphanRemoval = true,
47 | fetch = FetchType.LAZY)
48 | @JoinColumn(name = "employee_id")
49 | private Set docs;
50 | ```
51 |
52 | 存在列表查询中,分页查询 Employee 并转换为如下 Dto 的需求:
53 |
54 | ```java
55 | public class EmployeeDto {
56 |
57 | @ApiModelProperty(value = "员工id")
58 | private long id;
59 |
60 | @ApiModelProperty(value = "证件")
61 | private Set docs;
62 | ```
63 |
64 | 实现此需求的伪代码为:
65 |
66 | ```java
67 | List employees = employeeRepository.findByConditions(xx);
68 | return employees.stream().map(EmployeeDocDto::build).collect(Collectors.toList());
69 | ```
70 |
71 | 由于 Employee 下的 EmployeeDoc List 对象是懒加载的(懒加载是 ORM 中的一种推荐做法),在遍历查询出来的 N 个 Employee 转换成 EmployeeDto 的过程中,会再发起 N 次针对 EmployeeDoc 的查询,即一次查询带出了 N 个额外的查询,故称之为 N+1 问题。解决方案有:
72 |
73 | - 1 使用 JPA 中的 NamedEntityGraph 注解,以非懒加载的方式按预先设置的加载模版,加载出需要的所有数据。参考[教程](https://www.baeldung.com/spring-data-jpa-named-entity-graphs)。其局限是只能够非懒加载[一组子对象](https://stackoverflow.com/a/63044707/9304616)。
74 | - 2 使用 JdbcTemplate 首先查询出 Employee 相关数据,再以此为基础查询相关的 EmployeeDoc 数据,再在内存中进行组装。比较麻烦的地方是需要额外定义一些 BO。
75 | - 3 关闭 OSIV。
76 |
77 | 关闭 OSIV 值得拉出来单独探讨。关闭 OSIV 后,获取数据的时候需要显式地获取连接才能进行(在 @Transactional 注解范围内或者直接操控 EntityManager 对象)。对于使用 Spring Boot 默认配置被 OSIV 惯坏了的程序员,会惊讶于以下的简单代码会抛出 LazyInitializationException。
78 |
79 | ```java
80 | // 外层无事务
81 | Optional employee = repository.findById(1L);
82 | System.out.println(employee.get().getDocs());
83 | ```
84 |
85 | 这正是关闭 OSIV 的代价,也是 Spring Boot 开发人员心心念念要[默认开启 OSIV 的原因](https://github.com/spring-projects/spring-boot/issues/7107#issuecomment-260633493)——让程序开发更符合直觉和简单,但是掩盖了背后的真正行为。
86 |
87 | ### 2.2 异步线程中访问懒加载子对象失败问题
88 |
89 | 这是刚开始使用 Spring Data JPA 和 @Async 注解时容易遇到的问题,异常信息为:
90 |
91 | - org.hibernate.LazyInitializationException - could not initialize proxy - no Session
92 |
93 | 常见场景有:
94 |
95 | - 在主线程中取出了一个 Employee 对象,传递到一个 @Async 异步方法中,在该异步线程中访问了 Employee 下的 docs 等懒加载字对象。
96 | - 测试代码中取出对象并访问其一对多子对象,若未特殊处理,也会出现此问题。
97 |
98 | 同上一部分所述,被 Spring Boot 的 OSIV 惯坏了的程序员一开始面对此问题会手足无措。关闭 OSIV 的情况下此类型问题会更早被暴露。关于如何在关闭 OSIV 的情况下(以及在异步线程中)做一个负责任的程序员,正确的处理 LazyInitializationException,[这篇文章](https://vladmihalcea.com/the-best-way-to-handle-the-lazyinitializationexception/)有很好的解释,综合归纳为以下解决方案:
99 |
100 | - 1 和第一个问题一样,可以使用 EntityGraph 来精确控制每次要同时加载的字对象,而不用二次加载。限制仍然是只能加载一个一对多子对象。
101 | - 2 使用非懒加载,即 @OneToMany 中加上 fetch = FetchType.EAGER。[不推荐这种做法](https://vladmihalcea.com/the-best-way-to-handle-the-lazyinitializationexception/),会在很多情况下取出多余数据。
102 | - 3 在配置文件中设置 enable_lazy_load_no_trans: true。[不推荐这种做法](https://vladmihalcea.com/the-hibernate-enable_lazy_load_no_trans-anti-pattern/),这是一种非常丑陋的模式。
103 | - 4 在异步线程中加上 @Transactional 注解,或者使用其他方法开启数据库连接。不推荐将实体类在线程间传递,推荐在异步线程中接受 ID,并重新查询出实体类。
104 |
105 | ### 2.3 影响数据库性能(真的!)
106 |
107 | 考虑下面这段代码:
108 |
109 | ```java
110 | // 外层无事务
111 | Optional employee = repository.findById(1L);
112 | if (employee.ifPresent()) {
113 | // do remote call
114 | }
115 | ```
116 |
117 | 在开启 OSIV 的情况下,每个请求会在一开始就绑定一个 Session 对象,并在第一次执行数据库访问的时候为 Session 对象绑定一个数据库连接(从服务的数据库连接池中取一个),Session 在本次请求结束的时候自动关闭,并归还数据库连接。上面的代码中,我们的期望行为是:
118 |
119 | ```java
120 | // 外层无事务
121 | // 获取数据库连接并访问对象
122 | Optional employee = repository.findById(1L);
123 | // 归还数据库连接
124 | // 发起远程访问
125 | if (employee.ifPresent()) {
126 | // do remote call
127 | }
128 | ```
129 |
130 | 但是开启 OSIV 的实际行为是:
131 |
132 | ```java
133 | // 外层无事务
134 | // 获取数据库连接并访问对象
135 | Optional employee = repository.findById(1L);
136 | // 数据库连接随 Session 对象保持
137 | // 发起远程访问
138 | if (employee.ifPresent()) {
139 | // do remote call
140 | }
141 | // 数据库连接随 Session 对象保持
142 | ```
143 |
144 | 当有远程调用,如果这个远程调用需要消耗蛮长的时间(如 10s),那么只需要同时并发 10 个请求,就能让[默认配置数据库连接池配置](https://stackoverflow.com/a/55026845/9304616)(10 个连接)下的 Spring Boot 服务陷入无数据库连接可用的情况,带来巨大的性能问题。
145 |
146 | ## 3 一些推荐的代码写法
147 |
148 | - 在@Query 中返回 bool 值
149 |
150 | 使用 case when 语法:
151 |
152 | ```java
153 | /**
154 | * 根据航司代码查看是否存在(排除指定的id)
155 | */
156 | @Query("select case when count(id) > 0 then true else false end from CommonAirline where code = :code and id <> :exceptId")
157 | boolean existsByCodeAndExceptId(@Param("code") String code,
158 | @Param("exceptId") Long exceptId);
159 | ```
160 |
--------------------------------------------------------------------------------
/backend/java/如何使用 Java 根据 Html 生成 PDF 文档.md:
--------------------------------------------------------------------------------
1 | # 如何使用 Java 根据 Html 生成 PDF 文档
2 |
3 | 百度/必应/谷歌一下,使用 Java 生成 PDF 文档的常用工具为
4 |
5 | - [iText](https://github.com/itext/itext7)
6 |
7 | 但是最新的 `iText7` 使用 `AGPL` 协议,需要购买 license 才能够合理合法的在商业项目中使用。本着省钱的原则,使用 iText5 进行开发。
8 |
9 | ## 1 需求及痛点
10 |
11 | 调研 `iText` 的 Html 转 PDF 使用后,其主要问题如下
12 |
13 | - 由于 `iText` 并非国人开发,内置的字体是不支持中文字符渲染,需要引入额外的字体依赖
14 |
15 | 网上有不少教程解决了这个问题,但是随之而来引入了另一个问题
16 |
17 | - 由于大部分教程引入额外字体依赖时仅考虑了全中文的情况,使用的字体并不能对英文字体进行很好的渲染,造成英文字符错位难看
18 |
19 | ## 2 依赖配置
20 |
21 | ```text
22 | // for pdf rendering
23 | compile group: 'com.itextpdf', name: 'itextpdf', version: '5.5.13.1'
24 |
25 | // for pdf rendering
26 | compile group: 'com.itextpdf.tool', name: 'xmlworker', version: '5.5.13.1'
27 |
28 | // for chinese font in pdf rendering
29 | compile group: 'com.itextpdf', name: 'itext-asian', version: '5.2.0'
30 | ```
31 |
32 | ## 3 字体注册代码
33 |
34 | ```java
35 | public class PdfFontProvider extends XMLWorkerFontProvider {
36 |
37 | private static final Logger LOG = LoggerFactory.getLogger(PdfFontProvider.class);
38 |
39 | public PdfFontProvider() {
40 | super(null, null);
41 | }
42 |
43 | @Override
44 | public Font getFont(final String fontName, String encoding, float size, final int style) {
45 | BaseFont font = null;
46 | try {
47 | if (StringUtils.equals(fontName, "STSong-Light")) {
48 | font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
49 | } else {
50 | font = BaseFont.createFont(FontFactory.TIMES_ROMAN, FontFactory.defaultEncoding, true);
51 | }
52 | } catch (Exception e) {
53 | LOG.error("未找到 STSong-Light 字体库,可能是 com.itextpdf.itext-asian 依赖未载入");
54 | }
55 | return new Font(font, size, style);
56 | }
57 |
58 | }
59 | ```
60 |
61 | 上述代码会根据 html 节点的 style 属性的 `font-family` 属性配置,对指明使用 `STSong-Light` 字体的内容使用宋体进行渲染,而其它部分则会使用其内置的 TIMES_ROMAN 这一英文字体进行渲染。根据对字体的更多需求,可对这一类进一步定制,搭配 Html 实现更美观的字体渲染。
62 |
63 | ## 4 PDF 生成代码
64 |
65 | ```java
66 | public static File html2Pdf(String html, String outputPath) {
67 | try {
68 | // step 1
69 | Document document = new Document(PageSize.A4);
70 | document.setMargins(20, 20, 0, 0);
71 | // step 2
72 | PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(path));
73 | // step 3
74 | document.open();
75 | // step 4
76 | InputStream cssInput = null;
77 | XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8)), cssInput, new PdfFontProvider());
78 | // step 5
79 | document.close();
80 | LOG.info("PDF file: {} rendering successfully", outputPath);
81 | return new File(outputPath);
82 | } catch (IOException ex) {
83 | // do something
84 | } catch (DocumentException ex) {
85 | // do something
86 | }
87 | }
88 |
89 | ```
90 |
91 | ## 5 使用要点
92 |
93 | - 必须引入 `itext-asian` 依赖以支持中文字体
94 | - 待转换 Html 中的中文所在节点的属性一定要指定为 `style="font-family:STSong-Light"` 才能正常转换
95 | - 对于中英文夹杂的部分,可以对个别文字使用 span 标签包裹并指定 `font-family` 以达到精确字体渲染的目的
96 |
--------------------------------------------------------------------------------
/backend/java/如何使用Java根据Excel模板文件绑定数据生成新文件.md:
--------------------------------------------------------------------------------
1 | # 如何使用 Java 根据 Excel 模板文件绑定数据生成新文件
2 |
3 | todo:先参考账单下载的设计文档:https://github.com/cntehang/tmc-services/blob/master/docs/corp/download-corp-bill.md
--------------------------------------------------------------------------------
/backend/java/如何使用枚举.md:
--------------------------------------------------------------------------------
1 | # java 中枚举的使用
2 |
3 | [参考 PR](https://github.com/cntehang/pay-service/pull/47)
4 |
5 | ## 枚举概述
6 |
7 | Code Review 示例,目的是为了确定统一的 enum 的明明风格:
8 |
9 | - `PaidSuccess` 不需要值的时候这样命名
10 | - `PaidSuccess("支付成功")` 需要值的时候这样命名
11 | - `CNY`特殊情况采用
12 |
13 | **_约定: 存的值全部采用英文 name,而非其值_**
14 |
15 | Code Review 检查点之一:`各种类、字段的明明风格`
16 |
17 | 特殊情况可以将值定义成内部类:
18 |
19 | ```java
20 | Closed(Value.Closed);
21 |
22 | static class Value {
23 | public static final String Closed = "Closed";
24 | }
25 | ```
26 |
27 | PR 风格:`大部分情况下,PR代码多少,文件多少是其次,但是目的最好单一,方便他人review`
28 |
29 | 待讨论点:
30 |
31 | - `valueOf`方法:除非需要定义于 name 不一样的 value,其他情况下不需要定义.
32 | - 参数传递、处理的时候,采用 Enum,而不是用 String 来处理。
33 |
34 | - java 中的枚举本质上是一种常量,但与常量相比具有很多优点:
35 |
36 | - 枚举是强类型的,编译器会检查枚举项是否匹配,这可以大大减少出错的几率;
37 |
38 | - 枚举是可扩展的,每个枚举项除包含一个 int 型的序号和一个文本字面量以外,还可以为枚举定义多个域,并用一个自定义的构造函数初始化;
39 |
40 | - 枚举变量是单例的,可以直接使用双等号(==)比较是否相等,使代码更简洁。
41 |
42 | - 一个枚举值是个枚举对象,可以拥有基于对象的一些方法,非常好用。
43 |
44 | ## 枚举的使用示例
45 |
46 | 以机票订单状态为例,定义枚举如下:
47 |
48 | ```java
49 | public enum FlightOrderStatusEnum {
50 |
51 | Submitted,
52 |
53 | WaitTicket,
54 |
55 | }
56 |
57 | ```
58 |
59 | ```java
60 | public enum FlightOrderStatusEnum {
61 |
62 | // 如果需要翻译的时候,就设置值
63 | Submitted("待确认"),
64 |
65 | WaitTicket("待出票"),
66 |
67 | }
68 |
69 | ```
70 |
71 | 实体类中可以这样使用枚举:
72 |
73 | ```java
74 |
75 | public class FlightOrder {
76 |
77 | /**
78 | * 订单状态:建议使用JPA的@Enumerated将枚举与DB的String类型对应
79 | */
80 | @Enumerated(EnumType.STRING)
81 | @Column(nullable = false, length = 30)
82 | private FlightOrderStatus status = FlightOrderStatus.Submitted;
83 | }
84 | ```
85 |
86 | ## 使用建议
87 |
88 | - 将枚举保存到 DB 时使用字符串类型,不要使用枚举的序号;
89 | _原因:因为枚举的序号是按顺序生成的,如果使用序号,将来添加新的枚举值时可能会产生混乱。_
90 |
91 | - 枚举的使用范围仅限于项目内部,不要在 API 中暴露枚举类型(在 API 中使用字符串表示);
92 | _原因:在 API 中包含枚举类型将使引用此 API 的外部应用强依赖于此枚举类型,使得枚举的定义难以更改。_
93 |
94 | - 枚举项的扩展信息,比如名称,描述等附加信息,建议使用数据字典表来保存;
95 | _原因: 修改这些附加信息时,只需要修改表中的数据即可,不需要更改代码。_
96 |
97 | - 枚举和字符串的转换可以使用 java.lang.Enum, org.apache.commons.lang3.EnumUtils 中的一些辅助方法。
98 |
99 | - 大部分情况下,实体中的枚举字段语意上应该是一个值类型,即任何情况下都不应该为 null, 在初始化时就给定合适的初始值。
100 |
--------------------------------------------------------------------------------
/backend/java/如何实现通用模块功能并与特定产品业务解耦.md:
--------------------------------------------------------------------------------
1 | # 如何实现通用模块的功能,并保证与特定产品业务解耦
2 |
3 | ## 1 案例一:行程单打印模块,数据导出,需要实时查询订单状态
4 |
5 | ### 1.1 说明
6 |
7 | 目前行程单打印模块,涉及到的产品有国内机票和国际机票,所以查询的订单状态实际上分别对应了国内、国际机票的订单状态,order_status 字段分别来源于 flight_order 和 intflight_order 两张表。
8 |
9 | ### 1.1 错误示例
10 |
11 | 以前的做法是,创建行程单打印任务的时候,就分别将 flight_order.status 和 intflight_order.status 冗余过来,作为 order_status 快照,之后就再也不会更改了,这不满足需求。
12 |
13 | ### 1.2 方案一:维护行程单打印任务的 order_status 字段
14 |
15 | 仍然冗余 order_status 字段,不同的是,国内机票、国际机票所有需要改变订单状态的业务流程,都需要流转行程单打印任务的订单状态,耦合且容易遗漏。坏方案。
16 |
17 | ### 1.3 方案二:sql 解决
18 |
19 | 可以用 union 或者 case when,效率高,也不复杂,我也不好说是不是好的方案,但感觉本来就是复杂语句,在加上 union 或者 case when 会变得很难阅读、不好维护。
20 |
21 | ```sql
22 | select r.*, o.status from itinerary_print_record r
23 | inner join (select id, status from flight_order) o on o.id = r.order_id
24 |
25 | union all
26 |
27 | select r.*, o.status from itinerary_print_record r
28 | inner join (select id, status from intflight_order) o on o.id = r.order_id
29 | ```
30 |
31 | ### 1.4 方案三:查出数据后,在业务层绑定实时的状态
32 |
33 | 查了两次数据库,性能应该会略慢于上面那种,但是我觉得更解耦,看起来更舒服。
34 |
35 | ```java
36 | var results = recordJdbcRepository.exportRecords(params);
37 |
38 | if (CollectionUtils.isNotEmpty(results)) {
39 | // 获取各产品订单实时订单状态
40 | List bizOrderIds = results.stream().map(ItineraryPrintRecordExportBo::getOrderId).collect(Collectors.toList());
41 | Map ordersStatusMap = orderStatusDomainService.getOrdersOrderStatusMap(bizOrderIds);
42 |
43 | results.forEach(item -> {
44 | String status = ordersStatusMap.get(item.getOrderId());
45 | item.setOrderStatus(status);
46 | });
47 | }
48 | ```
49 |
50 | ## 2 案例二:邮递管理模块,需要查询特地产品业务的字段
51 |
52 | ### 2.1 说明
53 |
54 | 目前要邮递的内容涉及到行程单、发票、奖品,涉及到的产品有国内机票、国际机票、积分,需求想查机票业务内容,比如 pnr、票号,作为一个中立的模块,不希望在邮递记录中增加这俩字段。
55 |
56 | ### 2.2 方案:使用 search_helper 字段
57 |
58 | 仅仅为了查询,使用 search_helper 查询辅助字段,隶属不同业务产品的邮递记录,赋上需要的值,用于模糊查询。
59 |
60 | ```java
61 | /**
62 | * model
63 | */
64 | public class DistRecord {
65 | /**
66 | * 关键词检索
67 | * 机票行程单:存的是pnr和票号
68 | * 机票发票:存的是pnr和票号
69 | * 奖品:奖品名称
70 | */
71 | @Column(length = 500)
72 | private String searchHelper;
73 |
74 | /**
75 | * 构建关键词检索字段,国内机票
76 | */
77 | public void buildSearchHelper(FlightOrderSummary summary) {
78 | this.searchHelper = String.join(COMMA_STRING, summary.getPnrs(), summary.getTicketNos());
79 | }
80 | }
81 | ```
82 |
83 | ## 3 案例三:邮递管理模块,详情页需要展示对应的产品订单的费用明细
84 |
85 | ### 3.1 说明
86 |
87 | - 详情页需要展示费用明细,比如系统使用费,改签费,保险费等等,仍然不希望将特定业务的费用字段带进来。
88 |
89 | - 这个需求比上述情形更进一步,还需要展示出来。
90 |
91 | ### 3.2 方案:使用 json 表示金额
92 |
93 | - 好在我们不关心展示的是什么东西,只要能展示出来就可以了,那么可以使用一个字段,存 json,表示多个费用项,以及它们对应的金额,json 中有什么,前端就无脑循环展示出来就好。
94 |
95 | - feeName 表示费用名称,不同的产品业务根据自己的需要存值,这个问题的本质是,认为这些费用项也是数据,而不是字段。
96 |
97 | - 另外,`List feeItems`其实就是 `k-v`结构,可以换成`Map`。
98 |
99 | ```java
100 | /**
101 | * model
102 | */
103 | public class DistRecord {
104 | @Column(columnDefinition = "TEXT")
105 | @Convert(converter = DistRecordFeeInfoConverter.class)
106 | private DistRecordFeeInfo feeInfo;
107 | }
108 |
109 | public class DistRecordFeeInfo {
110 | // 费用项列表
111 | private List feeItems = new ArrayList<>();
112 | }
113 |
114 | public class DistRecordFeeItem {
115 | // 费用名称
116 | private String feeName;
117 |
118 | // 金额
119 | private BigDecimal amount = BigDecimal.ZERO;
120 | }
121 | ```
122 |
123 | ## 4 案例四:保险订单模块,需要列表查询、导出 pnr、票号、车次号等字段
124 |
125 | ### 4.1 说明
126 |
127 | - 很多产品都会有保险,但保险模块内部,是不希望关心机票和火车票等具体业务的,加字段是不可能的。
128 |
129 | ### 4.2 方案:使用 json 表示金额
130 |
131 | - 这个需求比上述的情形,又更进了一步,不但需要展示特定业务的数据,还要明确的知道展示的是什么东西。
132 |
133 | - 主体思路其实没差别,这次我们使用 Map 来演示。
134 |
135 | - Map 外面还要套一层对象,对象中提供 create 和 get 方法,外部只调用该对象的 get 方法,假装 get 的是数据库字段,而 Map 对外是屏蔽的。
136 |
137 | ```java
138 | /**
139 | * model
140 | */
141 | public class InsuranceOrder {
142 | @Column(columnDefinition = "TEXT")
143 | @Convert(converter = InsuranceBizOrderInfoConverter.class)
144 | private InsuranceBizOrderInfo bizOrderInfo;
145 | }
146 |
147 | public class InsuranceBizOrderInfo {
148 | private static final String ITEM_KEY_TRAIN_CODE = "trainCode";
149 |
150 | /**
151 | * 产品订单信息条目
152 | */
153 | private Map items = new HashMap<>();
154 |
155 | /**
156 | * 创建火车票产品信息。火车票保险对应到 passenger
157 | */
158 | public static InsuranceBizOrderInfo createForTrain(TrainOrderPassenger passenger) {
159 | Map bizOrderItems = new HashMap<>();
160 | bizOrderItems.put(ITEM_KEY_TRAIN_CODE, passenger.getOrder().getRoute().getTrainCode());
161 |
162 | var info = new InsuranceBizOrderInfo();
163 | info.items = bizOrderItems;
164 | return info;
165 | }
166 |
167 | /**
168 | * 获取 trainCode
169 | */
170 | @JsonIgnore
171 | public String getTrainCode() {
172 | return MapUtils.isEmpty(this.items) ? null : this.items.get(ITEM_KEY_TRAIN_CODE);
173 | }
174 | }
175 |
176 | /**
177 | * bo
178 | */
179 | public class InsuranceOrderBo {
180 | @ApiModelProperty(value = "车次号", example = "G123")
181 | private String trainCode;
182 |
183 | public void bindBizOrderInfo(InsuranceBizOrderInfo bizOrderInfo) {
184 | this.trainCode = bizOrderInfo.getTrainCode();
185 | }
186 | }
187 | ```
188 |
189 | ## 总结
190 |
191 | - 原则:设计通用模块、中立模块时,尽量不要耦合进其他产品业务的内容。
192 |
193 | - 当需要在中立模块,查询特定产品的数据,如何处理:
194 |
195 | - 如果只是作为条件检索,使用 search_helper 查询辅助字段,存入要模糊搜索的内容
196 | - 如果需要在页面展示,但不关心字段本身是啥,有啥展示啥,将`字段项:字段值`这个`k-v`结构序列化成 json,保存为一个字段就好
197 | - 如果还需要知道这个字段具体是啥,在上一步的基础上定义好字段项在 json 中的 key 常数,再多一层封装就好
198 |
199 | - 其他说明:
200 | - 状态类型的字段,不要冗余
201 |
--------------------------------------------------------------------------------
/backend/java/如何序列化xml格式的数据.md:
--------------------------------------------------------------------------------
1 | # 如何序列化 xml 格式的数据
2 |
3 | 统一采用这种方式来进行 API 的序列化和反序列化
4 | gradle 依赖:`compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8')`
5 |
6 | ## dto 样例
7 |
8 | ```java
9 | @Data
10 | @JacksonXmlRootElement
11 | public class WechatPlaceOrderDto implements Serializable {
12 | private static final long serialVersionUID = 2738646911267887473L;
13 |
14 | /**
15 | * 是 String(32) wxd678efh567hg6787 微信分配的小程序ID
16 | */
17 | @JacksonXmlProperty(localName = "appid")
18 | private String appId;
19 | /**
20 | * 是 String(32) 1230000109 微信支付分配的商户号
21 | */
22 | @JacksonXmlProperty(localName = "mch_id")
23 | private String mchId;
24 | /**
25 | * 否 String(32) 013467007045764 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB"
26 | */
27 | @JacksonXmlProperty(localName = "device_info")
28 | private String deviceInfo;
29 | }
30 |
31 | ```
32 |
33 | `JacksonXmlRootElement`注解标志在 class 上,`JacksonXmlProperty`注解标注在属性上,这样就可以通过 API 正常接受 xml 和返回 xml 格式的数据了。
34 |
35 | ## API 的设置
36 |
37 | ```java
38 | @PostMapping(path = "/test", produces = {"application/xml", "text/xml"})
39 | public WechatPlaceOrderDto test2(@RequestBody String body) {}
40 | ```
41 |
42 | ## RestTemplate 设置
43 |
44 | 一般情况下,不用单独设置 RestTemplate,但是有时候第三方不按正规方法做,就需要自主设置一下了
45 |
46 | 请求发送设置:
47 |
48 | ```java
49 | HttpHeaders headers = new HttpHeaders();
50 | headers.setContentType(MediaType.APPLICATION_XML); // 表示自己发送的是xml格式数据,会按照xml来序列化
51 | HttpEntity request = new HttpEntity(dto, headers);
52 | *** resultDto = restTemplate.postForObject(url, request, ***.class);
53 | ```
54 |
55 | 解析 response 设置:
56 |
57 | ```java
58 | //正常情况不用设置,有对应的message converter,但也有例外,如微信服务端,返回的是xml数据,但是media type设置的是text/plain,这样就导致不能够自动解析到对象,只能以字符串接收,然后手工解析,但是也可以设置自己的message converter。 这个只有在确定对方的返回数据时才可以使用。
59 | MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
60 | List types = new ArrayList<>();
61 | types.addAll(converter.getSupportedMediaTypes());
62 | types.add(0, new MediaType("text", "plain"));
63 | converter.setSupportedMediaTypes(types);
64 |
65 | List> converters = restTemplate.getMessageConverters();
66 | converters.add(0, converter);
67 | restTemplate.setMessageConverters(converters);
68 | ```
69 |
--------------------------------------------------------------------------------
/backend/java/常见的编码习惯.md:
--------------------------------------------------------------------------------
1 | # 记录常用的编码习惯,有助于在编码过程中形成统一、易于维护的代码
2 |
3 | - 定义常用的小方法
4 |
5 | - 为`model`添加`getIdStr`方法,理由:bo 层的 ID 用的时 String,model 层用的是 Long,需要经常转换。
6 |
7 | ```java
8 | /**
9 | * 获取String形式的ID
10 | * @return
11 | */
12 | @JsonIgnore
13 | public String getIdStr(){
14 | return String.valueOf(id);
15 | }
16 | ```
17 |
18 | - 同理,在 bo 内可以定义一个`getIdLong`的方法
19 |
20 | - 禁止使用.\*替换多个引入
21 |
22 | 对 ide 进行配置即可避免其自动使用._,在 idea 中如此配置 Preferences -> Editor -> Code Style -> Java -> Class count to use import with '_',修改数量即可,一般填个几百是没问题的
23 |
--------------------------------------------------------------------------------
/backend/java/数据字段约定.md:
--------------------------------------------------------------------------------
1 | # 系统设计中的字段约定
2 |
3 | todo 待周会讨论后约定
4 |
--------------------------------------------------------------------------------
/backend/java/时间的存储.md:
--------------------------------------------------------------------------------
1 | # 系统中时间的存储
2 |
3 | 默认情况下,系统运行的时区是 UCT 时区,但是,并不是所有的时间值都适合采用 UTC 来进行存储。对系统中时间的存储做如下约定:
4 |
5 | - 系统中生成的时间值,统一采用 `UTC` 时区的值,如 create time,update time
6 | - 系统中,需要用来计算的时间值,统一采用`UTC`时区的值,如 UATP 的有效值,每次 UATP 扣款时,均需要进行有效值的判断,所以这里也需要存储为 utc 格式的值
7 | - 与第三方对接时,第三方要求采用的时区,采用对方要求的时区值进行存储,如微信要求所有时间为北京时间
8 | - 系统中,仅作为字符串值进行存储的,不用进行时间转换,直接存储成原始字符串即可,如个人生日直接存储值就好
9 |
10 | todo: 持续不冲其他实际场景
11 |
--------------------------------------------------------------------------------
/backend/process/API设计规范.md:
--------------------------------------------------------------------------------
1 | # API设计规范
2 |
3 | ## 0. 原则
4 |
5 | - 使用RPC风格, 拒绝REST。
6 | - 只将HTTP作为通讯协议,正常情况下只使用HTTP状态码200。
7 | - 定义特定的业务代码作为操作的返回。
8 | - 使用Json格式,而非Xml。
9 |
10 | ## 1. 路由
11 |
12 | - 路由格式为 `https://{baseUrl}/{module}/{version}/{controller}/{action}`, 其中module是可选的,例如:
13 |
14 | ```java
15 | https://dev-tmc-services.teyixing.com/front/v1/flight/cancelApply
16 | ```
17 |
18 | - 为每个Controller添加一个对应的路由Router文件,并为Controller的每个Action方法添加对应的常量,而不是直接硬编码路由到每个方法上; Route中的每个常量值应与Controller中的方法名完全相同。
19 | - Controller的Action方法应该用动词或动词词组命名。
20 |
21 | ## 2. 请求参数
22 |
23 | - 请求方法只使用HTTP POST
24 | - 请求参数应该以json格式放在HTTP的body中
25 |
26 | - **例外** 对于简单情形(比如只有一两个简单类型参数)也可以使用URL Parameter来传递,但对于密码,金额等敏感数据必须放在body中传递。
27 |
28 | - 只传递操作必须的数据,不应该为了重用而传递多余的不需要的数据。
29 |
30 | ## 3. 返回
31 |
32 | - 返回格式的定义如下:
33 |
34 | ```javascript
35 | {
36 | code: number // success 0, business exception starts with 1
37 | message: string // message, 'OK' for 0. The message is user-oriented that might be shown to end user.
38 | debugMsg?: string // optional, message used for debugging, including request/context data.
39 | data?: object // optinal, the biz data, empty if code is not 0
40 | }
41 | ```
42 |
43 | - 返回状态码
44 | - 使用自定义的状态码表示业务操作的返回状态。
45 | - 0表示成功。
46 | - 1~96用来表示需要客户端关注的业务状态,这些状态码应该在API详细文档中有详细说明。
47 | - 97表示数据访问错误。
48 | - 98表示参数错误。
49 | - 99表示未知错误。
50 | - 100 以上的系统错误代码通常由系统中间件处理,可以有一份指导性的标准错误代码。 中间件被各个子系统共享,所以容易标准化。
51 |
52 | ## 4. 文档
53 |
54 | - 使用swagger来生成api文档, 参考 [swagger文档使用说明](./swagger-usage-guideline.md)
55 |
56 | ## 5. 测试
57 |
58 | - API开发时应先定义出接口,包括路由,传入和传出参数。
59 | - 在开发完成之前,应先生成模拟数据,模拟数据应定义为json文件。
60 | - 在开发完成之后,使用真实数据替代模拟数据。
--------------------------------------------------------------------------------
/backend/process/basic-service-developer-flow.md:
--------------------------------------------------------------------------------
1 | # 后端服务的开发基本流程
2 |
3 | 定开发后端服务的基本原则是为了能够更规范的开发我们的服务,也能适当减少沟通带来的成本。
4 |
5 | ## API类型的服务
6 |
7 | API类型的服务需要对外提供API,当前主要以REST 风格的API为主,后期可能会有其他类型的API(GRPC),此类型的服务的基本开发流程:
8 |
9 | 1. 定义API:定义需要对外输出的API,定义完成之后,对外输出swagger文档,同时需要相关人员进行review:主要review:API形式,model
10 | 2. 定义存储的数据结构:如果服务需要持久化数据,那么需要在这一步定义清楚需要持久化的数据结构,并明确表明每个字段的含义。主要review点:因我们大部分数据是来源于第三方,所以需要保证数据源的数据被合理的融合进了我们的数据
11 | 3. 设计业务逻辑:关键、复杂的业务处理,需要有设计相关文档,可以简单的画出业务执行流程,以及步骤定义。主要review点:逻辑是否可精简(保留核心流程,其他流程走异步之类的)
12 | 4. 代码实现:根据设计实现业务逻辑代码。这个部分不需要太多思考,按照设计去实现即可。主要review点:可以设计与实现是否合适
13 | 5. 测试用例:这个不要求代码覆盖率以及分支覆盖率,但是要求核心逻辑必须测全
14 |
15 | ## 独立运行的服务
16 |
17 | 独立运行的服务,如定时任务、消息处理任务等,次类任务,不需要对外提供API,但是可能需要持久化数据、或者对外发布消息等。与上面的服务类型相比,除了第一步不需要之外,其他的都需要
18 |
19 | ## 代码分支定义与使用
20 |
21 | 1. 如果服务是一个全新的服务,那么可以再第一版的开发中直接使用master分支
22 | 2. 服务一旦上线,需要保证master分支能够随时部署到线上去。也就是需要维持下面几个约定:
23 | 1. 每次新版本上线,打个TAG
24 | 2. 线上的bugfix,开个bugfix的分支,修复、测试完毕之后,merge到master,并上线
25 | 3. 新功能开发,以功能名命名一个分支,并在新分支上进行开发,测试完毕之后合并到master,进行上线
26 | 4. 版本回退,如果线上版本出了问题,需要回退,先保留master分支,然后将master分支回退到固定版本
27 |
--------------------------------------------------------------------------------
/backend/process/java-service-test.md:
--------------------------------------------------------------------------------
1 | # Java 微服务测试框架以及方法
2 |
3 | ## 单元测试
4 |
5 | 当前 Java 服务采用的是 Spock 进行测试
6 | 具体文档可以参考: [SPOCK官方文档](http://spockframework.org/)
7 |
8 | ### API测试用例
9 |
10 | - 如果API的条件是由严变松(如:not null --> nullable ),并增加null的测试(测试用例、测试脚本都可以)
11 | - 如果API的条件是由松变严(如:nullable --> not null),那么可以沿用之前的测试用例
12 |
13 | ### 其他原则
14 |
15 | - 测试数据以json文件存放,且json文件必须格式化
16 |
17 | ## 集成测试
18 |
19 | 当前 Java 服务集成测试采用的是 WireMock 框架,具体文档可以参考:[WireMock官方文档](http://wiremock.org/docs/)
20 |
21 | ###
--------------------------------------------------------------------------------
/backend/process/release-guideline.md:
--------------------------------------------------------------------------------
1 | # 发布流程
2 |
3 | 
4 |
5 | 在 test/prod 环境,需要进行代码与运行程序的版本化控制,目前项目使用 github 的 release 功能来做版本发布与管理,结合项目工程,制定该流程。
6 |
7 | ## 1. 发版流程
8 |
9 | 发布新版本的流程如下:
10 |
11 | 1. 发版
12 |
13 | 登录 github,在工程页面,点击 release -> Draft a new release -> 填写对应的 version tag(来自于 build_scripts/application_version.gradle 中的版本号)与 release title(与 version 一致),从 github Wiki 中把该版本的 Page 文档修改历史拷贝到 describe 中 -> Publish release,如下:
14 |
15 | 
16 |
17 | 1. 通知运维发布新版,并确定发版成功
18 | 1. 版本迭代
19 |
20 | 发布之后,发布人需要把当前版本号更新迭代,如 v2.0.1 -> v2.0.2,迭代涉及以下工作:
21 |
22 | - 修改 build_scripts/application_version.gradle 中的版本号
23 | - 新建该版本对应的 sql 目录
24 | - github 上,点击 Wiki -> New Page -> title 用版本号(版本号为最新的),初始化内容
25 | - 把上述修改 push 并 merge 到 master 中
26 |
27 | ### 特别注意
28 |
29 | - release Tag version 是打包时实际上采用的版本号,应与 application_version.gradle 中的版本号一致,且与 Wiki 中 title 的版本字段一致
30 | - release Title 与 release Tag version 保持一致
31 | - release 描述文字,复制 Wiki 中的**更新内容**和**回滚操作**中的条目对齐即可
32 |
33 | ## 2. 版本相关文件
34 |
35 | 在工程中,与版本相关的几个文件,特此说明:
36 |
37 | ### 2.1 build_scripts/application_version.gradle
38 |
39 | 打包相关的版本信息,内部记录了对应的版本号,发版之后需要按照流程迭代版本号
40 |
41 | ```text
42 | version = 'v2.0.1'
43 | jar {
44 | archivesBaseName = 'tmc-services'
45 | }
46 | ```
47 |
48 | ### 2.2 sql/vX_X_X
49 |
50 | ```text
51 | 对应版本需要执行的sql语句存放的目录,vX_X_X与版本号对应,由发版人员新建,开发人员在该版本使用的sql都存放在对应的目录下
52 | ```
53 |
54 | ### 2.3 Wiki Page
55 |
56 | 对应版本的修改历史,具体内容如下:
57 |
58 | ```text
59 | # 版本升级基本内容
60 | - 当前版本 v2.0.2
61 | - 上一版本 v2.0.1
62 | - 更新内容:
63 | - 登录流程中加入了验证码元素,当登录失败超过三次需要输入验证码,最后一次输入错误15分钟后可不用输入验证码
64 | - 回滚操作
65 | ```
66 |
67 | 以上所写内容应概括本次提交中的要点,需注意以下几点
68 |
69 | - 版本号由发版人在发布新版后迭代更新,与 application_version 中的版本号一致,在发布该版本之前不允许修改
70 | - 开发过程中,每个 pr 都需要如实记录提交的内容,并且带上 pr 链接与 issue 链接
71 | - 发布成功后,由发布人更新 Wiki Page 文档
72 |
--------------------------------------------------------------------------------
/backend/process/service-basic-rule.md:
--------------------------------------------------------------------------------
1 | # 服务开发的基本规约
2 |
3 | - 每个服务,只专注于一个功能模块的开发.
4 | - 在服务之间发送请求的时候,需要带上本地的 traceId,并将其置于 Header 中: “TraceId”
5 |
--------------------------------------------------------------------------------
/backend/process/swagger-usage-guideline.md:
--------------------------------------------------------------------------------
1 | # Swagger-usage-guideline
2 |
3 | - swagger 文档应成为前端调用后端接口的手册,在此对 swagger 使用进行规范,方便前后端交互。
4 |
5 | ## API 名称和说明
6 |
7 | - 在 Controller 中对外暴露的方法上加上如下注解
8 |
9 | ```java
10 | @ApiOperation(value = "API名称", notes = "API说明")
11 | ```
12 |
13 | - 若该 API 有需要指出的异常情况,则在 notes 中加以说明,如下:
14 |
15 | ```java
16 | @ApiOperation(
17 | value = "员工登录",
18 | notes = "根据用户名和密码进行登录认证"
19 | + "code = 1: 密码错误"
20 | + "code = 2: 用户名不存在"
21 | )
22 | ```
23 |
24 | - 有一种对返回值的写法如下,其会将 code 视为 Http Status Code,并不适合我们的情况,故不采用
25 |
26 | ```java
27 | @ApiResponses(value = {
28 | @ApiResponse(code = 1, message = "密码错误"),
29 | @ApiResponse(code = 2, message = "用户名不存在")
30 | })
31 | ```
32 |
33 | ## 请求参数说明
34 |
35 | - 参数若为类对象:
36 |
37 | - 类名上需加如下注释,用于说明此对象参数的名称:(这个注解完全没必要用)
38 |
39 | - value:对象参数名。特别注意:**value 不要用中文**,会导致 swagger 导出 json 失败
40 |
41 | ```java
42 | @ApiModel(value = "xxx")
43 | ```
44 |
45 | - 类中字段需在 @ApiModelProperty 注解中加如下说明:
46 |
47 | - value:表示字段名
48 | - example:表示该字段的示例值,在测试时很有帮助,但是,千万不要有单引号,遇到复杂类型,宁可不写 example
49 | - required: 若为必携带的参数,则为 true
50 |
51 | ```java
52 | @ApiModelProperty(value = "用户名,可以为手机/邮箱", example = "709091988@qq.com", required = true)
53 | ```
54 |
55 | - 若为普通参数:
56 |
57 | - 在方法的请求参数前,与 @RequestParam 并列
58 |
59 | ```java
60 | @ApiParam(value = “用户ID”, example = "1100020")
61 | ```
62 |
63 | ## 返回值说明
64 |
65 | - 与请求参数如出一辙
66 | - 暂未看到返回非对象的注解方法,不过返回非对象的情况很少,基本可以忽略
67 |
68 | ## Sagger 配置类示例
69 |
70 | ```java
71 | @Configuration
72 | @EnableSwagger2
73 | public class SwaggerConfig {
74 |
75 | /**
76 | * swagger配置
77 | * @return swagger相关配置
78 | */
79 | @Bean
80 | public Docket createRestApi() {
81 | return new Docket(DocumentationType.SWAGGER_2)
82 | .apiInfo(apiInfo())
83 | .select()
84 | .apis(RequestHandlerSelectors.basePackage("com.tehang.tmc.services.application.rest.front.corp"))
85 | .paths(PathSelectors.any())
86 | .build();
87 | }
88 |
89 | private ApiInfo apiInfo() {
90 | return new ApiInfoBuilder()
91 | .title("TMC Services")
92 | .description("出行从未如此简单")
93 | .contact(new Contact("TMC group", "https://www.teyixing.com", "admin@teyixing.com"))
94 | .version("1.0")
95 | .build();
96 | }
97 |
98 | }
99 | ```
100 |
101 | ## Swagger 文档界面说明
102 |
103 | 
104 |
--------------------------------------------------------------------------------
/backend/regulation/how-to-init-project.md:
--------------------------------------------------------------------------------
1 | # Java 服务开发
2 |
3 | ---
4 |
5 | ## 基本环境与开发工具
6 |
7 | - JDK: 1.8
8 | - Gradle: 4.8
9 | - IDE: IntelliJ IDEA
10 | - Mysql: 5.7
11 | - Redis: 4.0
12 | - Springboot:2.0.4
13 |
14 | ## 初始化项目
15 |
16 | - 在 github 上创建项目:项目名称采用小写单词加中划线命名
17 | - 创建时选择 private
18 | - 自动生成 readme.
19 | - 将 github 上的项目 clone 到本地:此时得到本地的一个空项目
20 |
21 | - 创建 gradle 项目,勾选 java、Groovy
22 | - GroupId:com.tehang.xxx.xxx
23 | - ArtifactId:xxx-xxx-xxx
24 | - 然后 IDE 会将项目会将项目初始化好
25 | - 添加 git ignore 文件:复制[git ignore](https://github.com/cntehang/public-dev-docs/blob/master/.gitignore)文件内容即可
26 | - 提交项目到 github
27 |
28 | `至此,项目就初始化完毕了.`
29 |
30 | ## 初始化项目结构
31 |
32 | ### 添加构建脚本
33 |
34 | 将之前构建脚本 copy(build_scripts 文件夹,与根目录下的 build.gradle)过来,并修改以下内容:
35 |
36 | - 依赖项(application_dependencies.gradle),保证只使用了本项目需要的依赖
37 |
38 | - 应用版本和名称:application_version.gradle
39 |
40 | - maven 仓库(build_setups.gradle),原有阿里云 maven 仓库地址()随时可能失效,请使用最新的地址()
41 |
42 | ### 创建代码基本包目录
43 |
44 | - 基本包目录
45 |
46 | ```bash
47 | -main
48 | - java
49 | - com
50 | - tehang
51 | - <项目名>
52 | - application : 对外的服务
53 | - builder : dto的构建方法
54 | - dto : 对外提供的model
55 | - rest : rest接口
56 | - service : 跨服务、跨domain的服务
57 | - domain : 业务逻辑
58 | - model : 数据model
59 | - repository : 数据库访问的入口
60 | - service : 业务逻辑
61 | - infrastructure
62 | - config : 项目配置项
63 | - exceptions : 异常定义与处理
64 | - filters : 一些过滤器,用于请求前、后的一些处理
65 | - routers : 路由定义
66 | - ... : 其他一些可能用到的包
67 | - utility : 常用工具类
68 | - Application.Java : springboot 启动入口
69 | - resources : 配置目录
70 | - application.yml : 公共配置
71 | - application-dev.yml : 针对开发环境的配置
72 | - application-test.yml : 针对测试环境的配置
73 | - application-pro.yml : 针对生产环境的配置
74 | -test
75 | - groovy
76 | - com.tehang.xxx.xxx : 单元测试目录
77 | - integration
78 | - groovy
79 | - com.tehang.xxx.xxx : 集成测试目录
80 | - resources : 集成测试资源文件目录
81 | - resources : 单元测试资源文件目录
82 | ```
83 |
84 | 创建好项目目录之后,基本完成了项目的搭建,然后就可以开始着手项目的实际业务开发。
85 |
86 | ### 项目 build 基本方式
87 |
88 | 项目采用 gradle 打包、编译,所以再开始编译前,需要大体了解 gradle 的几个关键命令:`gralde wrapper`,`gradlw clean build`
89 |
90 | - gradle wrapper : 打包编译用的 gradle 包
91 | - ./gradlew clean build : 编译项目
92 |
93 | 为规范代码标准,项目引入了 pmd、checkstyle 等代码规范检查插件(在 build_scripts/quality_assurance 目录),项目初始化时如已引入这些插件,建议更新这些插件。
94 |
95 | 更新方式:在项目的根目录执行下面的命令
96 |
97 | ```bash
98 | git submodule update --init --recursive
99 | ```
100 |
101 | 如没有引入或遇到问题,请按文档进行操作 [引入代码检查规则](./quality_assurance.md)
102 |
103 | ### 项目开发
104 |
105 | #### 项目的配置管理
106 |
107 | 我们的项目中,配置采用`yml`文件进行配置,并使用 springboot 的配置加载自动加载配置。后期可能会考虑使用 config center,目前而言,没有必要
108 |
109 | ```java
110 | @Value("${pkfare.timeout:60}")
111 | private long pkfareTimeout;
112 | ```
113 |
114 | 配置部分约定:
115 |
116 | - 根据不同运行环境,配置不同的配置:除所有运行环境均使用的配置需要放到`application.yml`文件里面外,其他的配置都放置到 application 里面去
117 | - 代码中,配置统一放到 config 目录的类中,进行统一管理,不允许单独注入到使用类当中
118 | - 不同类型的配置,放到不同的配置类中:如`IBEConfig.java`,`PkfareConfig.java`
119 |
120 | #### 日志
121 |
122 | 日志采用 sl4j + logback 记录日志。除了工具之外,还有几个比较中的:日志的格式、日志的记录地点、日志记录的内容、日志记录的级别、日志跟踪。
123 |
124 | - 日志格式:`%date [%thread] [%X{TraceId}] %-5level %logger{80}.%M - %msg%n`
125 | - 日志记录的地点: 记录到本地文件,不同类型日志,记录到不同文件,每日分割,最后日志由日志分析平台收集
126 | - 日志记录的内容:能够清晰的描述日志记录点的数据、动作、结果
127 | - 日志记录级别:线上只开通 info 级别,测试环境可以开通其他级别,后期开启动态调级的设置
128 | - 日志跟踪:每个请求进入系统时,都会在其处理线程中添加一个线程本地变量`TraceId`,记录日志时,自动采用本变量,如果需要创建线程做一些异步操作,或者需要调用其他服务,都请将 traceId 带上
129 |
130 | #### 异常处理
131 |
132 | 项目中,所产生的异常,我们都采用统一处理的方式进行处理,不能够在业务逻辑中把代码处理掉,但可以转换异常类型,抛出新的异常。
133 | 项目中,按照异常产生点,大体会分为两种类型的异常:业务前置异常、业务异常:
134 |
135 | - 业务前置异常:此类异常大体时因为参数验证失败、权限验证失败等因素导致,此时还没有进入到 controller 中去
136 | - 业务异常:这种类型的异常一般是因为业务处理失败,数据异常等因素导致
137 |
138 | 不同异常的处理方式:
139 |
140 | - 业务前置异常:不用封装,提供统一处理
141 | - 业务异常:项目中自定义异常类型,将业务中的异常情况转换成自定义异常,并统一处理
142 |
143 | ```bash
144 | 统一处理的最大的因素,是因为能够根据异常,返回不同的code到前端,而不用每个API自己处理
145 | ```
146 |
147 | #### 接口定义
148 |
149 | 所有后端项目采用 REST API 的方式对外提供服务,数据通过 body 返回,采用 json 格式,错误码以 Http Code 为准,针对特殊情况,再采用自定义 code.
150 | 自定义 code 以 http code 为基础,后面补两位座位自定义 code :
151 |
152 | 例如:403 的意思是`拒绝访问`的意思,但是拒绝的原因却可能有多种,例如没有权限,token 失效等等。针对不同原因,可以定义自定义 code:`40301`,`40302`等错误类型,用于定位不同的错误类型。最多可在一种 http code 下定义 99 种错误类型
153 |
154 | #### 服务内的执行流程
155 |
156 | filters -> controller -> applicationService -> domainService -> domainRepository
157 | `不允许逆向调用`
158 |
159 | #### 内部服务服务调用
160 |
161 | 因所有的内部服务都通过 Rest API 对外暴露,所以内部服务的调用都采用 Rest call:`RestTemplate`
162 |
163 | #### 外部服务调用
164 |
165 | 目前而言,项目中有不少的外部项目调用,统一采用`HttpClient`进行调用
166 |
167 | ## 单元测试与集成测试
168 |
169 | 初始化项目时需要引入单元测试、集成测试框架,具体步骤如下:
170 |
171 | 1. 在 build_scripts 目录下新建 test.gradle 文件.
172 |
173 | ````gradle
174 | // 单元测试与集成测试的相关配置
175 | apply plugin: 'org.unbroken-dome.test-sets'
176 |
177 | testSets {
178 | // 指定集成测试的目录
179 | integrationTest { dirName = 'test/integration' }
180 | }
181 |
182 | check.dependsOn integrationTest
183 | integrationTest.mustRunAfter test
184 |
185 | // 单元测试的配置,必须使用test task,指定了单元测试的category
186 | test {
187 | useJUnit {
188 | includeCategories 'com.tehang.tmc.services.UnitTest' //请根据项目实际的GroupId进行适当更改
189 | }
190 | testLogging {
191 | showStandardStreams = true
192 | }
193 | }
194 |
195 | // 自定义的集成测试task,指定了集成测试的category
196 | integrationTest {
197 | useJUnit {
198 | includeCategories 'com.tehang.tmc.services.IntegrationTest' //请根据项目实际的GroupId进行适当更改
199 | }
200 | testLogging {
201 | showStandardStreams = true
202 | }
203 | }
204 | ``` .
205 |
206 | 2. 修改build.gradle,加入以下内容:
207 |
208 | apply from: 'build_scripts/test.gradle'
209 |
210 | 这样在build项目的时候会自动进行单元测试、集成测试.
211 |
212 |
213 |
214 | 3. 加入接口与基类
215 |
216 | - 在test\groovy\com.tehang.xxx.xxx\目录下加入UnitTest接口文件及UnitTestSpecification基类文件.
217 |
218 | - 在test\integration\groovy\com.tehang.xxx.xxx\目录下加入IntegrationTest接口文件及IntegrationTestSpecification基类文件.
219 |
220 | 4. 单元测试
221 |
222 | 所有单元测试代码位于 test/groovy 目录下,按照要测试的类或方法,放在对应的类中,并且必须继承UnitTestSpecification类.
223 |
224 | 5. 集成测试
225 |
226 | 所有集成测试代码位于 test/integration/groovy 目录下,并且必须继承IntegrationTestSpecification类.
227 |
228 | 6. 资源文件
229 |
230 | 单元测试、集成测试可能会引入资源文件,请在相应的resource目录下添加对应的application.yml文件.
231 | ````
232 |
--------------------------------------------------------------------------------
/backend/regulation/quality_assurance.md:
--------------------------------------------------------------------------------
1 | # quality_assurance
2 |
3 | 代码检查规则,放到单独的 repo,所有项目公用
4 |
5 | ## 添加方式
6 |
7 | 先 cd 到项目的`build_scripts`目录下面,然后执行下面命令:
8 |
9 | ```shell
10 | git submodule add https://github.com/cntehang/quality_assurance.git
11 | ```
12 |
13 | 这样就把代码检查工具添加到你的目录下了,即可像其他的一样正常使用
14 |
15 | 假如执行上面命令的过程中遇到报错:`'build_scripts/quality_assurance' already exists and is not a valid git repo`
16 |
17 | 尝试在`build_scripts`目录下运行如下命令:
18 |
19 | ```shell
20 | rm -rf quality_assurance/
21 | git submodule add -f https://github.com/cntehang/quality_assurance.git
22 | ```
23 |
24 | ## 更新方式:
25 |
26 | 新 clone 下来的后台项目是不会默认 clone 子 module 的,所以`quality_assurance`还没有 clone,所以需要在项目的根目录执行下面的命令
27 |
28 | ```shell
29 | git submodule update --init --recursive
30 | ```
31 |
32 | 执行完之后,`quality_assurance`就会 clone 到本地项目目录下,如此即可正常使用了
33 |
34 | 后面如果`quality_assurance`有更新,执行同样的命令即可.
35 |
--------------------------------------------------------------------------------
/backend/regulation/tmc-services项目review小记.md:
--------------------------------------------------------------------------------
1 | # tmc-services项目review小记
2 |
3 | ## 自动出票
4 |
5 | 目前自动出票的流程大致是,下单之后会执行“预记账”,“出差审批”,“超标授权”,“自动订座”,每个任务执行完成后需要判断是否需要创建出票任务,如果需要则创建自动,并且会推送一条消息到 MQ 中,tmc 会消费自动出票消息,如果可以自动出票会给资源平台推送一条消息,资源平台拿到这条消息以后,会创建出票任务。定时任务每 10 秒钟会调用自动出票接口,随机选出一个出票任务自动出票
6 |
7 | ### 下单后的任务处理接口
8 |
9 | 下单之后的任务处理有些没有考虑幂等性问题,比如当前“超标授权”的接口,如果某个订单最后一个任务是“超标授权”任务,由于网络原因,同一个订单被用户点了两次审批请求,或者其他原因导致有两个针对同一个订单的“审批”请求。在创建自动出票任务时,两个同样的请求执行到`FlightOrder order = flightOrderRepository.findByIdEnsured(orderId);`,两个线程通过拿到的订单信息判断出自动出票任务都没有被创建,就会导致同一个订单创建两个自动出票任务。
10 |
11 | ```java
12 | @Transactional
13 | public Long createTicketConfirmTaskIfRequired(long orderId) {
14 | LOG.info("Enter createTicketConfirmTaskIfRequired. orderId: {}", orderId);
15 | Long taskId = null;
16 | FlightOrder order = flightOrderRepository.findByIdEnsured(orderId);
17 | LOG.debug("requireTicketConfirm(订单状态,付款状态、PNR状态、审批状态、授权状态)? : {}", requireTicketConfirm(order));
18 | if (requireTicketConfirm(order) && !taskRepository.existsByOrderIdAndTaskType(order.getId(), TICKET_CONFIRM)) {
19 | //创建出票任务
20 | FlightTask task = createTicketConfirmTask(order);
21 | taskRepository.save(task);
22 | taskId = task.getId();
23 | }
24 | LOG.info("Exit createTicketConfirmTaskIfRequired. taskId: {}", taskId);
25 | return taskId;
26 | }
27 | ```
28 |
29 | ### 创建自动出票任务
30 |
31 | 只有最后一个完成任务的线程会创建自动出票任务,目前 B 方案的设计,如果最后一个任务执行完成,但是创建自动出票任务失败会导致“死单”的出现,可以考虑在订单中增加一个“待出票”状态或者其他方式来解决
32 |
33 | ### 处理自动出票消息
34 |
35 | tmc 在处理自动出票消息通过订单状态来判断解决消息幂等性可能会有问题,但是又不能单纯的通过消息的 id 来处理消息幂等性问题,目前是根据订单状态来判断消息是否被处理过,如果正在处理某个订单的一条自动出票的消息,但是此时这个订单的状态还没被更新,又拿到了一条这个订单的自动出票消息,就会导致这个订单重复出两张票
36 |
37 | ### 处理自动出票任务消息
38 |
39 | 目前我们处理自动出票任务是通过定时任务的方式,定时任务使用的是 spring 的 scheduled,这个是单线程的,如果某个任务”阻塞“,会导致后面的任务都延期执行,并且如果任务堆积,目前处理自动出票任务是随机选出一个订单,有可能导致先完成的订单一直都没有被随机到。
40 | 一种比较好的处理方式是在消费 tmc 自动出票任务消息的时候就处理自动出票任务,但是这么处理就需要通过控制同一个 group 下的消费者的数量和线程池中线程的数量来控制自动出票任务消息的消费速度
41 |
42 | ## 代码缓存
43 |
44 | 代码中缓存使用的比较少,在"运营管理","基础数据","系统管理"这些模块很多数据都是配置一次,后期基本不会更新,使用缓存带来的收益会很大。经常查询的接口“查航班信息接口”,“订单查询”的接口等等都可以加缓存,之前和 davis 讨论过,有一些缓存策略还是很复杂的,这部分需要详细设计
45 |
46 | 推荐一些缓存相关的文章:
47 |
48 | - [缓存相关知识](https://app.yinxiang.com/shard/s47/nl/13163762/845a3580-d409-4a76-92c5-a6289adef114/)
49 | 这篇文章包括了在缓存使用过程中“缓存穿透”,“缓存击穿”,“缓存雪崩”等等概念的介绍和解决方案
50 |
51 | - [Redis 深度历险:核心原理与应用实践](https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b336601f265da598e13f917)
52 | 很多文章写关于 redis 的文章或者书都会包括很多运维相关的知识,这本小册子里面干货很多,特别适合开发看,包括布隆过滤器,限流,LRU,分布式锁,介绍的都很清楚,里面的例子都有 java 版本的,推荐去看一下
53 |
54 | ## 代码中的事务处理
55 |
56 | 官方文档上说`Transactions are atomic units of work that can be committed or rolled back`,是一组工作的原子提交或者回滚,和`ACID`是紧密结合在一起的。
57 | 我的理解也不是很深刻,但是我认为有些代码事务太大了,有些不合理,例如`placeOrder`接口事务就用的比较大,查找员工信息,加载航班信息,这些应该都没有必要放到下单这个事务里。
58 |
59 | ```java
60 | @Transactional
61 | public PlaceOrderResult placeOrder(PlaceOrderRequestBo request, Long bookingEmployeeId, Company company, boolean fromAdmin, Long staffId, String staffName) {
62 | LOG.info("bookingEmployeeId: {}", bookingEmployeeId);
63 |
64 | Employee bookingEmployee = employeeRepo.findByIdEnsured(bookingEmployeeId);
65 |
66 | //加载航班信息
67 | loadFlightInfo(request);
68 |
69 | //下单
70 | PlaceOrderResult placeOrderResult = doPlaceOrder(request, company, bookingEmployee, fromAdmin, staffId, staffName);
71 |
72 | LOG.info("Exit placeOrder. orderId: {}", placeOrderResult.getOrder().getId());
73 | return placeOrderResult;
74 | }
75 | ```
76 |
77 | 另外消费自动出票消息,是否有必要整个消费过程都加一个事务,`FlightTask task = getFlightTask(body);`,是否有必要加锁?
78 |
79 | ```java
80 | @Override
81 | @Transactional
82 | public void consume(String tag, String key, String body) {
83 | LOG.info("Enter. body(taskId): {}", body);
84 |
85 | FlightTask task = getFlightTask(body);
86 | try {
87 | LOG.debug("task.isWasSystem() => {}, task.getTaskStatus() => ", task.isWasSystem(), task.getTaskStatus());
88 | if (!isTaskConsumedRepeatedly(task)) {
89 | attachSupplierInfoToOrder(task);
90 | doTicketing(task);
91 | }
92 | } catch (Exception ex) {
93 | LOG.error("Exception happen. ex: {}", ex.getMessage(), ex);
94 | handleTicketingExceptionCase(task);
95 | }
96 |
97 | LOG.info("Exit.");
98 | }
99 | ```
100 |
101 | ## MQ 的使用
102 |
103 | MQ 虽然具有序解耦,异步等优点,但是 MQ 也会让系统变得复杂。MQ 并不能保证消息 100%的被正确投递,“丢消息”,“重复投递”都有可能会发生,目前我们的代码都没有考虑这些情况的发生。如果丢消息真实发生了,最好要有状态记录和补偿机制
104 |
105 | 推荐一门课程:
106 |
107 | - [RabbitMQ 消息中间件技术精讲](https://coding.imooc.com/class/chapter/262.html#Anchor)
108 | 这个是一门视频课程,虽然讲的是 RabbitMQ,但是这门课的第三章还是有很多干货,里面包括了如何保证消息 100%被投递成功,“大厂”在使用 MQ 的时候的解决方案,限流等等
109 |
110 | ---
111 |
112 | > 下面都是杆精,说的不一定对
113 |
114 | ---
115 |
116 | ## 状态变量的存储
117 |
118 | 我认为在数据库中存储状态变量,没必要直接存储字符串,直接使用`0,1,2,3...`等状态码,没必要直接使用状态变量的文案。
119 | 首先这部分文案会占用很多数据库空间,存这部分字符串占用的空间是存状态码的几十倍,其次后期订单操作历史数据变多以后,对数据库性能也会有影响
120 |
121 | ```java
122 | private void recordErrorInfo(FlightOrder order) {
123 | order.addOrderHisByAdmin(TaskAssigner.SYSTEM_STAFF_NAME, "申请自动出票失败", "发送自动出票消息时发生异常,转入人工处理流程");
124 | flightOrderRepository.save(order);
125 | }
126 | ```
127 |
128 | ## 代码中的硬编码
129 |
130 | 比如说下面代码,1,2,6 分别代表什么?新人写代码的时候并不知道`Applicationexception`,哪些 code 被别人使用过,新的 code 应该用多少。
131 |
132 | ```java
133 | /**
134 | * 重设 Identity Step 1 => 检验密码和原identity并发送验证码
135 | *
136 | * @param resetCheckBo 相关参数
137 | */
138 | public void resetIdentityCheck(IdentityResetCheckBo resetCheckBo, long curEmployeeId) {
139 | validateCodeApplicationService.checkValidateCode(resetCheckBo.getValidateToken(), resetCheckBo.getValidateCode());
140 | try {
141 | employeeIdentityResetDomainService.preCheckAndSendVerifyCodeForIdentityReset(resetCheckBo, curEmployeeId);
142 | } catch (EmailNotCorrectException ex) {
143 | throw new ApplicationException(2, ex.getMessage());
144 | } catch (MobileNotCorrectException ex) {
145 | throw new ApplicationException(1, ex.getMessage());
146 | } catch (PasswordNotCorrectException ex) {
147 | throw new ApplicationException(6, ex.getMessage());
148 | }
149 | }
150 | ```
151 |
152 | ## 重复的代码
153 |
154 | 有些接口存在任务重复执行情况,比如”orderPrebilling“,消费"自动出票"消息等代码都有一些地方重复执行了`FlightOrder order = flightOrderRepo.findByIdEnsured(orderId);`,针对同一个订单,查一次以后就可以直接在代码里面传递`order`,没必要多次查库,会降低接口性能。
155 |
156 | ## ID 的处理
157 |
158 | 目前我们有些订单 id 是通过数据库自增的方式,性能比较差,并且订单量容易被看出来。有些地方用了"snowflake"方案,还是建议单独独立出来一个”发号“服务,便于业务拓展。
159 |
160 | ```java
161 | @Transactional
162 | public String getNextSeqValue(String seqName) {
163 | LOG.debug("Get next sequence value with seqName: {}", seqName);
164 |
165 | sequenceRepository.incrementSequence(seqName);
166 | long result = sequenceRepository.getCurrentSequenceValue(seqName);
167 |
168 | LOG.debug("Get next sequence value of: {} with result: {}", seqName, result);
169 | return String.valueOf(result);
170 | }
171 | ```
172 |
173 | 推荐好文:
174 |
175 | - [美团点评分布式 ID 生成系统](https://juejin.im/entry/58fb22655c497d0058f5febb)
176 |
177 | ## 参数的传递
178 |
179 | 代码中一些参数的传递太大,比如说下面几个例子,其实只是需要`PlaceOrderRequestBo`中的某个字段,没必要把整个对象传进去,在阅读代码的时候会很困惑。
180 |
181 | ```java
182 | /**
183 | * 创建订单乘机人列表
184 | */
185 | @LoggerAnnotation
186 | public List createOrderPassengers(PlaceOrderRequestBo request, FlightOrder order, Company company, Employee bookingEmployee) {
187 |
188 | List passengers = new ArrayList<>();
189 | int seqNo = 0;
190 |
191 | for (PassengerBo passengerBo : request.getPassengers()) {
192 | FlightOrderPassenger passenger = createOrderPassenger(passengerBo, order, company, bookingEmployee, seqNo);
193 | //passenger.setSeqNo(++seqNo);挪到创建乘客过程中,避免子订单号创建出错
194 | passenger.setOrder(order);
195 | passengers.add(passenger);
196 | ++seqNo;
197 | }
198 | return passengers;
199 | }
200 |
201 | /**
202 | * 根据下单请求参数添加或更新员工信息
203 | * @param request 下单请求参数
204 | */
205 | @Transactional
206 | public void updateEmployeeInfoIfRequired(PlaceOrderRequestBo request, long corpId) {
207 | LOG.debug("Enter");
208 |
209 | request.getPassengers().forEach(passengerBo -> {
210 | if (StringUtils.equalsIgnoreCase(passengerBo.getPassengerType(), FlightPassengerType.ADULT)) {
211 | updateEmployeeInfoIfRequired(passengerBo, corpId);
212 | }
213 | });
214 | LOG.debug("Exit");
215 | }
216 |
217 | /**
218 | * 创建订单行程列表
219 | *
220 | * @param request
221 | * @param order
222 | * @param fromAdmin
223 | * @return
224 | */
225 | @LoggerAnnotation
226 | public List createOrderRoutes(PlaceOrderRequestBo request, FlightOrder order, FlightConfig flightConfig, boolean fromAdmin) {
227 |
228 | List routes = new ArrayList<>();
229 | int seqNo = 0;
230 |
231 | for (RouteBo routeBo : request.getRoutes()) {
232 | FlightOrderRoute route = createOrderRoute(routeBo, flightConfig, fromAdmin);
233 | route.setSeqNo(++seqNo);
234 | route.setOrder(order);
235 | routes.add(route);
236 | }
237 | return routes;
238 | }
239 | ```
240 |
--------------------------------------------------------------------------------
/backend/regulation/后端测试.md:
--------------------------------------------------------------------------------
1 | # 后台测试说明
2 |
3 | 目前后台测试包括单元测试与集成测试,对此做一下说明:
4 |
5 | - 单元测试
6 |
7 | 针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法
8 |
9 | - 集成测试
10 |
11 | 整合测试又称组装测试,即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。
12 |
13 | ## 所用技术
14 |
15 | - [Spock](http://spockframework.org/spock/docs/1.3-RC1/index.html):测试框架
16 | - Groovy:所用语言(与 java 兼容,可以混用)
17 | - [H2](http://www.h2database.com/html/main.html):嵌入式数据库,用于替代 MySQL,**_集成测试使用_**
18 | - [embedded-redis](https://github.com/kstyrc/embedded-redis):嵌入式 redis,**_集成测试使用_**
19 | - [wiremock](http://wiremock.org/):Api Mock 工具,用于拦截对外请求,**_集成测试使用_**
20 | - [gradle-testsets-plugin](https://github.com/unbroken-dome/gradle-testsets-plugin):gradle 插件,用于区分单元测试与集成测试
21 |
22 | ## 整体说明
23 |
24 | ### 区分两种测试
25 |
26 | 项目引入`gradle-testsets-plugin`插件,用于区分单元测试与集成测试,做法如下:
27 |
28 | 1. 引入插件
29 |
30 | 在 buildscript -> dependencies 中加入如下语句
31 |
32 | ```groovy
33 | classpath( 'org.unbroken-dome.gradle-plugins:gradle-testsets-plugin:1.4.2')
34 | ```
35 |
36 | 2. 区分两种测试
37 |
38 | 在`gradle`文件中加入如下配置,用于标志区分两种测试
39 |
40 | ```groovy
41 | // 单元测试与集成测试的相关配置
42 | apply plugin: 'org.unbroken-dome.test-sets'
43 |
44 | testSets {
45 | // 指定集成测试的目录
46 | integrationTest { dirName = 'test/integration' }
47 | }
48 |
49 | check.dependsOn integrationTest
50 | integrationTest.mustRunAfter test
51 |
52 | // 单元测试的配置,必须使用test task,指定了单元测试的category
53 | test {
54 | useJUnit {
55 | includeCategories 'com.tehang.resource.train.UnitTest'
56 | }
57 | testLogging {
58 | showStandardStreams = true
59 | }
60 | }
61 |
62 | // 自定义的集成测试task,指定了集成测试的category
63 |
64 | integrationTest {
65 | useJUnit {
66 | includeCategories 'com.tehang.resource.train.IntegrationTest'
67 | }
68 | testLogging {
69 | showStandardStreams = true
70 | }
71 | }
72 | ```
73 |
74 | - `test/integration`是指定的集成测试代码的目录,而单元测试代码仍然放在`test/groovy`目录下,这是默认目录,不需要指定
75 |
76 | - `com.tehang.resource.train.UnitTest`是单元测试使用的标志接口,所有单元测试都实现这个接口,实际做法是有一个`UnitTestSpecification`实现这个接口,然后继承这个类
77 |
78 | - `com.tehang.resource.train.IntegrationTest`是集成测试使用的标志接口,所有集成测试都实现这个接口,实际做法是有一个`IntegrationTestSpecification`实现这个接口,然后继承这个类
79 |
80 | 3. 结构说明
81 |
82 | 仅对`test`目录说明
83 |
84 | ```text
85 | test
86 | |-groovy 单元测试代码所在目录,内部为package结构,与程序对应
87 | |-integration
88 | |-groovy 集成测试代码所在目录,内部为package结构,与程序对应
89 | |-resources 继承测试使用的配置或资源
90 | |-resources 单元测试使用的配置或者资源
91 | ```
92 |
93 | PS:问题点
94 |
95 | ## 如何写单元测试
96 |
97 | 1. 新建`groovy`类,继承`UnitTestSpecification`
98 |
99 | 2. Mock 该类依赖的外部类
100 |
101 | 3. 构建数据,编写测试用例
102 |
103 | 例子:
104 |
105 | ```groovy
106 | class ApprovalRejectDomainServiceSpec extends UnitTestSpecification {
107 |
108 | ApprovalRepository approvalRepo = Mock(ApprovalRepository)
109 | FlightOrderRepository orderRepo = Mock(FlightOrderRepository)
110 | FlightTaskRepository taskRepo = Mock(FlightTaskRepository)
111 | FlightOrderCancelConfirmDomainService cancelConfirmDomainService = Mock(FlightOrderCancelConfirmDomainService)
112 |
113 | ApprovalAuditRejectDomainService auditRejectDomainService =
114 | new ApprovalAuditRejectDomainService(approvalRepo, orderRepo, cancelConfirmDomainService, taskRepo)
115 |
116 | def loadFromJson(String fileUrl) {
117 | def json = new String(new File(fileUrl).getBytes())
118 | Approval approval = com.tehang.tmc.services.utility.JsonUtils.toClass(json, Approval.class)
119 | approval.getApprovalHis().each {
120 | his ->
121 | his.setApproval(approval)
122 | }
123 | return approval
124 | }
125 |
126 | def "审批拒绝测试"() {
127 | given: "给定审批参数"
128 |
129 | long employeeId = 1
130 | String employeeName = ""
131 | Approval approval = loadFromJson("src/test/resources/json/corp/approval/approval_simple.json")
132 | ApprovalAuditBo bo = new ApprovalAuditBo()
133 | bo.setAuditPassed(false)
134 |
135 | orderRepo.getFlightOrdersByApprovalId(_) >> Arrays.asList()
136 |
137 | when: "审批拒绝"
138 | auditRejectDomainService.auditReject(employeeId, employeeName, bo, approval)
139 |
140 | then: "审批单状态为已拒绝"
141 | approval.status == ApprovalStatus.REJECTED
142 | }
143 |
144 | }
145 | ```
146 |
147 | ## 集成测试
148 |
149 | 集成测试相对于单元测试,是更进一步的测试,会把项目运行起来,然后把内部所有模块都集成起来测试,需要满足以下条件:
150 |
151 | 1. 不依赖于外部服务,能独立完成所有测试
152 |
153 | 目前我们使用外部服务,只要是通过发送 http 请求以及 MQ 通讯,其中 http 请求使用 WireMock 来进行拦截以及模拟返回,而 MQ 则使用 MockBean 把 producer 和 consumer 都 mock 起来
154 |
155 | 2. 每次运行都是独立的,不会受上一次的影响
156 |
157 | 运行间独立主要是数据方面的问题,为此引入嵌入式数据库和缓存,每次运行都是新的环境,避免影响
158 |
159 | ### 如何写集成测试
160 |
161 | 1. 新建`groovy`类,继承`IntegrationTestSpecification`
162 |
163 | 2. 引入 WireMockRule,并且制定需要 stub 的地址
164 |
165 | ```groovy
166 | String insuranceResponseStr = new FileReader('src/test/resources/json/insurance/insurance.json').text
167 | stubFor(post(urlPathMatching("/v1/insurance/all"))
168 | .willReturn(aResponse().withHeader("Content-Type", "application/json")
169 | .withHeader("Connection", "close")
170 | .withStatus(200)
171 | .withBody(insuranceResponseStr)))
172 | ```
173 |
174 | 建议建一个 Stub 类,把这个 stub 放着这个类中,在使用的地方引入即可
175 |
176 | ```groovy
177 | class FlightStub {
178 |
179 | static void stubForSearchFlightResponse() {
180 | def searchResponseJson = new FileReader('src/test/resources/json/flight/integration/shopping_response.json').text
181 | stubFor(post(urlPathMatching("/v1/flight/shopping"))
182 | .willReturn(aResponse().withHeader("Content-Type", "application/json")
183 | .withHeader("Connection", "close")
184 | .withStatus(200)
185 | .withBody(searchResponseJson)))
186 | }
187 | }
188 | ```
189 |
190 | ```groovy
191 | def "Test1: search"() {
192 | given:
193 |
194 | HttpHeaders headers = new HttpHeaders()
195 | headers.add("Authorization", "Bearer " + loginResponse.body.data.token)
196 |
197 | //1 查询车票
198 | def searchRequest = slurper.parse(new FileReader('src/test/resources/json/flight/integration/shopping_request.json'))
199 | HttpEntity httpEntity = new HttpEntity(searchRequest, headers)
200 |
201 | FlightStub.stubForSearchFlightResponse()
202 |
203 | FlightStub.stubForInsurance()
204 |
205 | when:
206 | flightSearchResponse = restTemplate.exchange("/front/v1/flight/searchFlights", HttpMethod.POST, httpEntity, Object.class)
207 |
208 | then:
209 | flightSearchResponse.status == 200
210 | flightSearchResponse.body.code == 0
211 | }
212 | ```
213 |
214 | 3. 编写测试用例
215 |
216 | 为了集成度更高,建议直接对接口测试,使用 RestTemplate 直接对接口发起请求
217 |
218 | ```groovy
219 | @Autowired
220 | private TestRestTemplate restTemplate
221 |
222 | def "Test0: login"() {
223 | given:
224 |
225 | def loginBo = slurper.parse(new FileReader('src/test/resources/json/employee/login.json'))
226 |
227 | when:
228 | loginResponse = restTemplate.postForEntity("/front/v1/employee/login", loginBo, Object.class)
229 |
230 | then:
231 | loginResponse.status == 200
232 | loginResponse.body.code == 0
233 | }
234 | ```
235 |
236 | ## 最佳实践
237 |
238 | ## 处理静态方法的模拟
239 |
240 | 根据 Peter Niederwieser (Spock 框架主要作者) 的[回答](https://stackoverflow.com/questions/15824315/mock-static-method-with-groovymock-or-similar-in-spock),要想模拟 java 代码中的静态方法,必须引入其它依赖。在当前情况下要想测试包含静态方法调用的代码,建议绕过“模拟静态方法”这个点,使用集成测试进行(静态方法在集成测试环境下能正常运行)。
241 |
--------------------------------------------------------------------------------
/backend/resources/akka-flowgraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/akka-flowgraph.png
--------------------------------------------------------------------------------
/backend/resources/java_layer_depencies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/java_layer_depencies.png
--------------------------------------------------------------------------------
/backend/resources/java_project_layer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/java_project_layer.png
--------------------------------------------------------------------------------
/backend/resources/jet-workflow-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/jet-workflow-example.png
--------------------------------------------------------------------------------
/backend/resources/jet-workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/jet-workflow.png
--------------------------------------------------------------------------------
/backend/resources/release_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/release_example.png
--------------------------------------------------------------------------------
/backend/resources/swagger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/swagger.jpg
--------------------------------------------------------------------------------
/backend/resources/workflow-vs-microservice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cntehang/public-dev-docs/f1c5d8d65cd11fa8185580797ed6a80ab8ecfff1/backend/resources/workflow-vs-microservice.png
--------------------------------------------------------------------------------
/coding-guide/README.md:
--------------------------------------------------------------------------------
1 | # 编程指南
2 |
3 | 常见编程工作的指南。
4 |
5 | - [Git 协作指南](./git-workflow.md)
6 | - [Git 提交信息指南](./how-to-write-commmit-message.md)
7 | - [Code Review 指南](./how-to-review-code.md)
8 | - [前端代码审查列表](./fe-code-review-check-list.md)
9 | - [项目版本号约定](./how-to-control-version.md)
10 | - [如何写日志](./how-to-log.md)
11 | - [错误处理指南](./how-to-handle-error.md)
12 | - [项目 Readme 模版](./sample-project-readme.md)
13 |
--------------------------------------------------------------------------------
/coding-guide/fe-code-review-check-list.md:
--------------------------------------------------------------------------------
1 | # 前端代码审查列表
2 |
3 | - [ ] 功能设计/代码结构是否合理
4 | - [ ] 文件行数/函数行数是否超标(150/30)
5 | - [ ] Http 请求的错误处理
6 | - [ ] Http 请求状态与的 spin 或者 button 状态的关联
7 | - [ ] 关键步骤的日志
8 | - [ ] 新增的组件都需要开启 OnPush 策略
9 | - [ ] 组件销毁时的资源释放( unsubscribe, clearInterval 等)
10 | - 特别需要注意的是通过 service 得到的 observable
11 | - [ ] 禁止魔数(number string)
12 | - [ ] 变量命名需要有意义,禁止:i j str obj 等
13 | - [ ] 类型准确,没有足够的理由禁止使用 any
14 | - [ ] 对于 Array, map/some/every/find/filter > forEach > for loop , 优先使用语义明确的方法
15 | - [ ] 使用解构让代码变得更简洁
16 |
17 | ```diff
18 | - const data = this.getData()
19 | - if (data.xxx) { do something}
20 | - if (data.yyy) { do something}
21 | - return {
22 | - xxx: data.xxx,
23 | - yyy: data.yyy,
24 | - zzz: data.zzz,
25 | - }
26 | + const {xxx, yyy, zzz} = this.getData()
27 | + if (xxx) { do something}
28 | + if (yyy) { do something}
29 | + return {xxx, yyy, zzz}
30 | ```
31 |
32 | - [ ] 避免傻瓜代码
33 |
34 | ```diff
35 | - const result = this.isValid()
36 | - if (result) {
37 | - return true
38 | - }
39 | - return false
40 | + return this.isValid()
41 | ```
42 |
--------------------------------------------------------------------------------
/coding-guide/git-workflow.md:
--------------------------------------------------------------------------------
1 | # Git 协作指南
2 |
3 | 基于一个流行的 [项目开发指南](https://github.com/elsewhencode/project-guidelines),本文描述了一个建议的 github 工作流程。和上述指南最大的不同是我们采用 develop 分支作为主开发分支。另外创建专门的上线发布 master 分支。这样的好处是 develop 是 `master` 的下游分支,变基,PR 和合并都比较自然。而且上线发布分支只有少数人关注,对开发人员越隐蔽越好。
4 |
5 | 具体项目可以定制工作流程。项目初期可以采用简化的工作流。比如,一个项目在初期只有一、二个人的时候,所有操作都在 develop 分支 -- 但是上线前必须有 master 分支。
6 |
7 | 分支类型:
8 |
9 | - master 分支:该分支在第一次上线之后,永远保持可随时上线状态,不允许直接在这个上面进行更改或者提交代码,只能从其他分支 merge。通过权限设置保持 `master` 分支中的代码稳定性。
10 | - develop 分支:develop 分支主要用于新版本的开发,代码第一次上线时,从 develop 创建出 master 分支用于上线发布。所有后期版本开发都在 develop 分支上进行。版本开发完毕之后,上线前,将 develop 的修改合并到 master,并做上线测试
11 | - 功能分支:每个版本的开发,会有很多独立功能,每个独立功能的开发,从 develop 创建一个功能分支,并在功能分支上进行功能的开发,最后变基合并到 develop。
12 | - hotfix 分支:这种类型的分支是针对线上 bug 的紧急 fix,直接从 master 创建分支。修复,测试完毕之后,合并到 master 上线。然后相应应该也并入 develop 分支。
13 |
14 | 一些原则:
15 |
16 | - PR 每个 PR 都只是针对一个功能,不能够太大, 原则上不超过 10 个文件
17 |
18 | ## 1. 七条基本规则
19 |
20 | 这里有七条基本规则需要牢记和遵守:
21 |
22 | - 规则一:保护您的 `develop`,尤其是 `master` 分支改动需要特别授权。
23 |
24 | 为什么
25 |
26 | > 这样可以保护您的生产分支免受意外情况和不可回退的变更。所有代码需要 PR review 后才能并入这二个分支。
27 |
28 | - 规则二:在功能分支中执行开发工作。
29 |
30 | 为什么
31 |
32 | > 因为这样,所有的工作都是在专用的分支而不是在主分支上隔离完成的。它允许您提交多个合并请求 `pull request`(PR)而不会导致混乱。您可以持续迭代提交,而不会使得那些很可能还不稳定而且还未完成的代码污染 develop 分支。
33 |
34 | - 规则三:请使用合并请求(Pull Request)将功能分支合并到 `develop`。不允许直接合并。
35 |
36 | 为什么
37 |
38 | > 通过这种方式,它可以通知整个团队他们已经完成了某个功能的开发。这样开发伙伴就可以更容易对代码进行 code review,同时还可以互相讨论所提交的需求功能。
39 |
40 | - 规则四:在发起合并请求之前,请确保您的功能分支可以成功构建,并已经通过了所有的测试(包括代码规则检查)。
41 |
42 | 为什么
43 |
44 | > 因为您即将将代码提交到这个稳定的分支。而如果您的功能分支测试未通过,那您的目标分支的构建有很大的概率也会失败。此外,确保在进行合并请求之前应用代码规则检查。因为它有助于我们代码的可读性,并减少格式化的代码与实际业务代码更改混合在一起导致的混乱问题。
45 |
46 | - 规则五:在发起合并请求 PR 前,请更新您本地的`develop`分支并且完成交互式变基操作(interactive rebase)。发起 PR 之前解决完潜在的冲突
47 |
48 | 为什么
49 |
50 | > rebase 操作会将功能分支合并到被请求合并的 develop 分支,并将您本地进行的提交应用于所有历史提交的最顶端,而不会去创建额外的合并提交(假设没有冲突的话),从而可以保持一个漂亮而干净的历史提交记录。 [合并(merge)和变基(rebase)的比较](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)
51 |
52 | - 规则六:请确保在 PR 合并分支后删除本地和远程功能分支。
53 |
54 | 为什么
55 |
56 | > 如果不删除需求分支,大量僵尸分支的存在会导致分支列表的混乱。而且该操作还能确保有且仅有一次合并到`develop`。只有当这个功能还在开发中时对应的功能分支才存在。
57 |
58 | - 规则七:给出清晰的提交信息(commit message)。具体要求参见后门的建议。
59 |
60 | 为什么
61 |
62 | > 这些信息给出清晰的开发历史和版本发布信息。有很大的代码维护价值。
63 |
64 | ## 2. 建议的工作流
65 |
66 | 基于以上原因, 我们将 [功能分支工作流](https://www.atlassian.com/git/tutorials/comparing-workflows#feature-branch-workflow) , [交互式变基的使用方法](https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing) 结合一些 [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows#gitflow-workflow)中的基础功能一起使用。 主要步骤如下:
67 |
68 | ### 2.1 项目初始建立`develop` 和 `master`二个分支
69 |
70 | - 针对一个新项目, 在 github 上创建项目的 repo,同时用 github 界面创建 `master` 发行分支仅用于新版本发布上线。 `clone`到本地后,只基于 `develop` 分支做开发:
71 |
72 | ```sh
73 | git clone <项目地址> # clone the remote repository
74 | ```
75 |
76 | ### 2.2 创建功能分支做具体开发
77 |
78 | - 第一步:检出(Checkout)一个新的功能或故障修复(feature/bug-fix)分支或用 github 界面基于`develop`创建功能分支。下面是命令行创建功能分支并同步到服务器。随时用 `git branch -a`检查当前分支状态。
79 |
80 | ```sh
81 | git checkout -b my-feature # create a new branch my-feature from the current branch
82 | git push -u origin my-feature # sync to remote server
83 | git branch -a # display branch status
84 | ```
85 |
86 | - 第二步:在功能分枝上进行新功能的开发,提交代码并随时同步到远程 Git 服务器做备份。
87 |
88 | ```sh
89 | git add . # Add all local changes
90 | git commit -a # commit all changes
91 | git push # push to remote frequently to bakcup changes
92 | ```
93 |
94 | 为什么
95 |
96 | > `git commit -a` 会独立启动一个编辑器用来编辑您的说明信息,这样的好处是可以专注于写这些注释说明。请参考下面关于说明信息的要求。经常同步到远程库做备份。通常在 IDE 里执行上面三个操作也可以,注意当前分支为功能分支就好。
97 |
98 | ### 2.3 合并功能分支到 `develop` 分支
99 |
100 | 当功能开发完成时,要将所做工作变基并入 `develop` 分支。
101 |
102 | - 第一步:在准备提交合并前, 先将需要 `develop` 分支更新到最新。下面的步骤建议手工运行。
103 |
104 | ```sh
105 | git checkout develop
106 | git pull
107 | ```
108 |
109 | 为什么
110 |
111 | > 更新 `develop` 代码版本有助于发现可能的冲突。当您进行(稍后)变基操作的时候,保持更新会给您一个在您的机器上解决冲突的机会。这比(不同步更新就进行下一步的变基操作并且)发起一个与远程仓库冲突的合并请求要好。
112 |
113 | - 第二步:切换至功能分支,把功能分支变基到`develop`分支,建议采用`rebase -i --autosquash`的交互方式
114 |
115 | ```sh
116 | git checkout my-feature
117 | git rebase -i --autosquash develop
118 | ```
119 |
120 | 为什么
121 |
122 | > 您可以使用 `--autosquash` 将所有提交压缩到单个提交。没有人会愿意(看到) `develop` 分支中的单个功能开发就占据如此多的提交历史。 [更多请阅读...](https://robots.thoughtbot.com/autosquashing-git-commits)
123 |
124 | - 第三步(可能需要):这一步在没有代码冲突可以跳过。如果您有代码合并冲突, 就需要[解决它们](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/)。
125 |
126 | ```sh
127 | git add ... # 任何必要的增删改后,加入修改
128 | git rebase --continue # 继续刚才的变基操作
129 | ```
130 |
131 | - 第四步:推送您的功能分支到 github。变基操作会改变提交历史, 所以您必须使用 `-f` 强制推送到远程(功能)分支。如果其他人与您在该分支上进行协同开发,请使用破坏性没那么强的 `--force-with-lease`。
132 |
133 | ```sh
134 | git push -f
135 | ```
136 |
137 | 为什么
138 |
139 | > 当您进行 rebase 操作时,您会改变功能分支的提交历史。下一步的合并请求(PR)是基于远程库进行的。这一步把本地的变基操作及修改同步到远程库。由于变基会导致 Git 拒绝正常的 `git push` 。能使用 `-f` 或 `--force` 或当多人在同一分支合作时用 `--force-with-lease` 参数了。[更多请阅读...](https://developer.atlassian.com/blog/2015/04/force-with-lease/)
140 |
141 | - 第五步:提交一个合并请求(Pull Request)。Pull Request 会被负责代码审查的同事和测试人员接受,组内人员负责审查代码质量,测试人员验证功能的正确性。两名人员接受之后才能合并和关闭。合并请求完成同时需要删除远程的功能分支。这些操作都利用 github 的用户界面进行。如果代码需要进一步的修改完善,请回到第一步。
142 |
143 | 为什么
144 |
145 | > 只有经过授权的人做代码的合并维护可以保护代码库的稳定可靠。代码是开发团队最宝贵的资源。
146 |
147 | - 第六步:合并完成后,记得删除您的本地分支。
148 |
149 | ```sh
150 | git checkout develop
151 | git branch -d <分支>
152 | ```
153 |
154 | (使用以下代码)删除所有已经不在远程仓库维护的分支。
155 |
156 | ```sh
157 | git checkout develop
158 | git fetch -p
159 | git branch -D featureBranch
160 | ```
161 |
162 | ### 2.4 版本发布到`master`分支
163 |
164 | 由程序员、运维人员和项目经理共同决定发布的时机。具体流程另外描述。
165 |
166 | 发版的人先从github发起一个从 `develop` 到 `master` 分支的 `PR`,并且把这个 `PR` 发给产品和项目经理review,产品主要负责审查发版的内容是否符合预期,如果符合预期通知发版的人发demo环境进行验证,验证通过之后,产品再接受这个 `PR`。如果遇到develop分支有些feature并不想都发版,那可以基于develop分支checkout一个`prerelease` 分支,在这个 `prerelease` 分支上面去掉那些不需要发版的commit,再提一个从prerelease分支到master分支的PR
167 |
168 | ```sh
169 | git checkout -b prerelease develop
170 | git checkout master
171 | git pull
172 | git checkout prerelease
173 | git rebase -i master
174 |
175 | # 手动删掉不准备发版的commit
176 | git rebase --continue
177 |
178 | # 在github提一个从prerelease到master的PR
179 | ```
180 |
181 | 为什么
182 |
183 | > 有可能几个feature是同时进行也都合并进了develop分支,但是产品可能暂时只想发其中某一些feature,这时候我们需要单独把这些feature拎出来合并进master,所以需要一个中间分支prerelease分支
184 |
185 | ### 2.5 hotfix
186 |
187 | hotfix 通常是指线上环境出现的需要紧急修复的bug,通常先从`master`分支切一个`hotfix`分支出来,测试验证通过再合并进去,然后确保这个修复已经同步到`develop`分支
188 |
189 | ```sh
190 | git checkout master
191 | git pull
192 | git checkout -b hotfix-train-ticket
193 |
194 | # github提PR到master分支,合并之后继续一下的步骤
195 | git checkout develop
196 | git pull
197 | git checkout -b fix-train-ticket
198 | git cherry-pick ${commitID} #commitID为hotfix分支对应的commitid
199 |
200 | # github提PR到develop分支,请关联issueID(如果有的话)和之前到master分支的PR ID
201 | ```
202 |
203 | 为什么
204 |
205 | > 所有的热修复都需要立即上线去解决用户的困扰的,所以需要马上合并到master,而develop很可能有新的feature暂时不能发版,所以需要通过cherry-pick把这些修复单独同步到develop,防止下次发版的时候把master上面的修复覆盖掉。
206 |
207 | ## 3 如何写好 Commit Message
208 |
209 | 坚持遵循关于提交的标准指南,会让在与他人合作使用 Git 时更容易。这里有一些经验法则 ([来源](https://chris.beams.io/posts/git-commit/#seven-rules)):
210 |
211 | - 用新的空行将标题和主体两者隔开。
212 |
213 | 为什么
214 |
215 | > Git 非常聪明,它可将您提交消息的第一行识别为摘要。实际上,如果您尝试使用 `git shortlog` ,而不是 `git log` ,您会看到一个很长的提交消息列表,只会包含提交的 id 以及摘要(,而不会包含主体部分)。
216 |
217 | - 将标题行限制为 50 个字符,并将主体中一行超过 72 个字符的部分折行显示。
218 |
219 | 为什么
220 |
221 | > 提交应尽可能简洁明了,而不是写一堆冗余的描述。 [更多请阅读...](https://medium.com/@preslavrachev/what-s-with-the-50-72-rule-8a906f61f09c)
222 |
223 | - 标题首字母大写。
224 | - 不要用句号结束标题。
225 | - 在标题中使用 [祈使句](https://en.wikipedia.org/wiki/Imperative_mood) 。
226 |
227 | 为什么
228 |
229 | > 与其在写下的信息中描述提交者做了什么,不如将这些描述信息作为在这些提交被应用于该仓库后将要完成的操作的一个说明。[更多请阅读...](https://news.ycombinator.com/item?id=2079612)
230 |
231 | - 使用主体部分去解释 **是什么** 和 **为什么** 而不是 **怎么做**。
232 |
233 | 更多关于 commit 的内容请参考[Write Good Commit Message](./how-to-write-commit-message.md)
234 |
235 |
236 | ## 4 总结Git工作流如下
237 |
238 | 1. feature分支往develop合并采用squash策略,需要测试人员验证通过approve
239 | 2. 发版的时候develop分支往master分支合并,采用merge策略
240 | 1. 开发人员从develop分支(或者prerelease分支)提PR到master
241 | 2. 产品审核PR信息,确认发版内容
242 | 3. 确认通过之后通知开发人员发demo环境
243 | 4. demo验证通过,产品接受PR
244 | 5. 开发merge这个PR,删除prerelease分支(如果有)
245 | 3. 发版操作由每个仓库的owner来负责,需要更新代码中的version和打tag
246 | 4. hotfix需要基于master分支切出来,合并进去之后通知发版的人,并且同时基于最新的develop切一个修复分支出来,cherry-pick刚刚那个修复的commit,再合并回develop,分支的合并过程同上
247 |
--------------------------------------------------------------------------------
/coding-guide/how-to-control-version.md:
--------------------------------------------------------------------------------
1 | # 版本号规约
2 |
3 | 版本号指的是项目的版本号,每次发布上线,版本号会同步有更新。版本号有很多种规范,技术部所有项目的版本号,按照一下规范制定:
4 |
5 | - 发布的版本,只能递增,不能回退,除非出现上线失败,回滚的场景
6 | - 版本号编制:x.y.z
7 | - x 是大功能版本迭代,例如业务流程大规模变更,或者界面大规模更新
8 | - y 是小的特性功能分支,例如上线了一个火车票功能
9 | - z 是小版本功能, 例如修复了一个 bug,完成了一个小的功能
10 | - x, y 由产品经理决定来制定,z 由开发人员制定
11 | - 每次增加 x 或者 y 的时候,项目需要卡 release 分支,z 版本迭代只需要合并到其所在的 y 分支以及 master
12 | - 不同项目的版本号不必同步
13 |
14 | ## 示例
15 |
16 | 例如 TMC,当前版本为 2.0.0,产品经理约定,优化(网金社、邮件等)预定为 2.1,火车票模块的功能可以预定为 2.2,优化发布上线后,将版本号从 2.0.0 升级为 2.1.0,并建立 2.1 的版本分支,此后如果有此版本的 bug fix,那么从 2.1 分支拉取分支,并提交到 2.1 和 master 分支,此时的版本号为 2.1.1
17 |
18 | ## 异常情况处理
19 |
20 | - 回滚:版本号不变更,直接会滚到上一个 release
21 | - 版本开发延迟或提前:如火车票先于优化模块完成,那么火车票就占用 2.2,并将优化的预定分支更新为 2.3
22 |
23 | ## 可能遇到的问题
24 |
25 | - 多版本并行开发的情况
26 | - 解决方式:谁先发布(merge 到 master),谁就占用版本号, 不提前规划版本号,开发分之以功能替换
27 | - 功能大小的界定,可能火车票作为一个大版本(X)发布,这个由根据开发小团队而定
28 |
--------------------------------------------------------------------------------
/coding-guide/how-to-handle-error.md:
--------------------------------------------------------------------------------
1 | # 错误处理指南
2 |
3 | 错误处理是最容易忽略但是对业务系统至关重要的功能。
4 |
5 | ## 基本原则
6 |
7 | - 对用户展示有用的错误信息。最好有准确的错误原因和建议的措施。
8 | - 错误信息分成二类:用户可以采取措施的信息和程序员可用的调试信息。后者是系统管理员和开发人员用的,可以隐藏。
9 |
10 | ## 错误代码
11 |
12 | 错误代码和错误信息用在二个地方:用户交互界面和服务之间的 API。这二个地方都适用上面的基本原则。API 的调用者也是用户。有个通用原则是要在界面和对外(跨进程)的 API Catch 所有的错误,返回一个错误代码和尽可能准确、简短的错误信息。给用户返回未经处理的错误不但不友好,还可能会泄露数据。
13 |
14 | 在 API 调用之间,有很多层次。比如简单的二层(多层类似):应用层调用网络层的服务发送请求和接受数据。每层有自己的错误处理。错误代码也要各自独立。
15 |
16 | 一套 API 牵涉到多个业务模块。相应的错误代码也可分为二类:一类是标准化的错误代码,另一类是业务模块自己独立的错误代码。
17 |
18 | 标准的错误代码统一编制,用来标示通用的和业务无关的错误信息或共同种类的错误。比如所有模块用 0 表示成功。用 900 到 999 表示共同的错误种类或统一处理的业务逻辑。比如请求参数错误用 901, 没有访问权限 912, 错误地址 922 等。这样方便在客户端和服务端用共同框架处理这类错误。
19 |
20 | 业务模块的错误代码自行定制。比如错误代码 2, 在用户系统表示用户名重复,在订单系统可能是订单过期。业务模块的客户端根据具体业务场景和错误代码给出正确的错误信息。
21 |
22 | ## 错误处理
23 |
24 | 软件都分成多个层级。高层代码的调用底层的函数/方法。这种上下层调用的错误处理非常简单:如果调用者知道如何处理错误,则捕获并处理这种错误。一个常见的例子是应用层知道网络超时需要重试。如果不知道怎么处理则不用捕获,由最上层(API 的对外出入口)代码或用户界面代码来做处理。
25 |
--------------------------------------------------------------------------------
/coding-guide/how-to-log.md:
--------------------------------------------------------------------------------
1 | # 如何写日志
2 |
3 | 日志也是程序的基本组成部分,和业务代码/错误处理代码一样。
4 |
5 | 对于分布式系统,很多时候日志是唯一有效的调试方法。一个典型的程序包括三分之一正常业务处理逻辑,三分之一异常业务处理,还有三分之一是 log 代码。 Log 代码在不同级别/不同详细程度记录了系统的运维运行状态和调试数据。
6 |
7 | 鉴于日志的重要性及不改变运行代码的要求,所有日志手工禁止用 AOP 这类工具来写日志信息。
8 |
9 | ## 日志目的
10 |
11 | 日志是运行时代码调试工具,有二个主要功能,通过不同的日志级别和运行状态来完成。
12 |
13 | - 错误报警:程序员和运维人员用 Error,Warning 和 Info 来知道错误发生和错误现场信息。缺省运行级别是 Info,可以知道运行的状态和错误/警告的发生。
14 | - 跟踪/定位:当错误出现后不能通过静态代码检查发现错误原因,需要在运行时打开 Debug 甚至 Trace 来跟踪定位错误。运行时没有 Debug 级别的信息输出,需要在要诊断的业务模块设置 Debug 级别来产生日志信息,用于事后跟踪调试下一次错误的发生。
15 |
16 | ## 日志的基本原则
17 |
18 | - 日志的主要用户是程序员和运维人员,和业务人员无关。
19 | - 任何错误/异常发生的地方都要用日志记录。
20 | - 因为有可能发送设计时无法估计的错误,在系统边界一定有 catch all exception 的日志,级别为 Error。第一次出现就需要在内部处理并给予合适的级别。
21 | - 仔细规划日志的级别,如果下面的通用指南不够清楚,请在业务模块文档或前后端给出特别的日志级别指南。
22 |
23 | ## 日志级别的使用指南
24 |
25 | 日志有五个级别:Error, Warning, Info, Debug, Trace。其定义和用途如下。
26 |
27 | ### Error
28 |
29 | Error 表示严重错误,系统异常或应用程序功能无法执行。比如未知的系统运行错误、不能连接到数据库、调用参数错误或严重业务数据错误。Error 级别的错误属于高优先级 bug,需要开发人员立即修复。
30 |
31 | 系统出现意料之外的异常也要用 Error 处理。因为 Unknown 的异常很可能非常严重,需要搞清楚每个 Unknown 的异常。
32 |
33 | ### Warning
34 |
35 | Warning 表示不影响程序继续运行或可以重试的各种系统错误,比如网络超时、不重要的数据错误、外部服务请求失败等。运维人员需要每周留意 Warning 信息,看是否有异常情况。
36 |
37 | ### Info
38 |
39 | Info 表示一个重要的系统事件。可以给系统运维人员提供重要的系统运行状态和性能统计数据。Info 事件不包括业务层面的事件,比如用户创建、订单的增删该查等。常见的 Info 事件有:
40 |
41 | - 系统的生命周期:启动、初始化、停止等。
42 | - 系统动态状态改变:动态配置改变、切换备用服务等。
43 | - 过去一小时/一天的请求数目,平均请求时间等。
44 |
45 | ### Debug
46 |
47 | Debug 是调试的主要级别。这个级别的信息应该给出完整的执行路径和重要的执行结果。具体要求如下:
48 |
49 | - 执行路径指函数调用和执行分支。函数调用时要么调用者,要么被调用者记录日志,但是不用重复记录。同时各个重要执行分支都用 Debug 日志记录分支的判断条件。
50 | - 打印的信息不应该太详细(比如有十个以上的属性),也不应该用在重复十次以上的循环内部数据。
51 |
52 | ### Trace
53 |
54 | Trace 给出详细的程序运行状态。Trace 可以用在循环的内部或用于打印完整的详细信息。当输出详细信息时,通常也先有一个 Debug 级别的摘要信息。比如,Debug 信息给出数组的尺寸,而 Trace 级别给出具体的数组数据(所有元素或一部分元素)。在非常底层不重要的分支,也可以不用 Debug 而用 Trace 输出日志。
55 |
56 | ## 日志格式
57 |
58 | 日志是给系统运维和开发人员看的。所以给出的信息也是以程序调试为主。常见二种格式
59 |
60 | ```java
61 | // 格式一
62 | 'user 1234 clicked on the save button on the sign-up page'
63 |
64 | // 格式二
65 | 'userId:1234 clicked on buttonId:save on pageId:sign-up'
66 | ```
67 |
68 | 第二种格式给出了具体的变量名称和对应状态值,是推荐的日志格式。即参数名和参数值之间用':'分隔。
69 |
70 | Debug 级别的日志在跨进程函数出入口进行记录时应成对出现。 推荐格式如下:
71 |
72 | ```java
73 | // "Enter. "作为推荐的函数进入点的日志格式标准,后面可以加上关键参数的信息
74 | "Enter GetOrder. orderId: 1234, employeeId: 37"
75 |
76 | // "Exit"作为推荐的函数退出点的日志格式标准
77 | "Exit GetOrder"
78 |
79 | // 当有返回值时,也可以记录返回的参数描述
80 | "Exit GetOrderCount. Return value: 42"
81 | ```
82 |
83 | ## 日志的使用效果
84 |
85 | 一个投入运行的生产系统,缺省的日志运行级别是 Info。可以看到系统的大概运行状态。
86 |
87 | - 平常应该很少见到 Error 级别的错误。正式运行时,应该是一个月难得一见。
88 | - Warning 级别的日志可能每天碰到,但是应该反应当时的网络状态或外部服务的稳定性。
89 | - Info 级别的日志代表了系统的状态改变或环境的变化。必要时也可以给出运行性能的统计数据。
90 | - Debug 用于反映出完整的程序执行路径和所用到的数据,但是不包含过数据细节。
91 | - Trace 用于补充 Debug 数据的细节。比如很大的数据或循环里的数据。
92 |
93 | ## 日志最佳实践
94 |
95 | - 对于失败的状态,在抛出异常的地方要记录日志,根据错误程度级别分别为 Error、Warning、Info、Debug。具体级别参考上面的解释。
96 |
97 | - 跨进程服务的 API 需要有 Debug 级别日志成对记录请求参数和返回结果,这样也提供了相应时间记录。
98 |
99 | - 日志语句中不要调用耗时的方法(在关闭日志以后,日志对性能的影响应该可以忽略不记)
100 |
101 | ```java
102 | 1. logger.debug("Enter. request:{}", JsonUtils.toJson(params));
103 | 2. logger.debug("Enter. request:{}", params);
104 | 3. if (logger.isDebugEnabled()) {
105 | "Enter. request:{}", JsonUtils.toJson(params));
106 | }
107 | ```
108 |
109 | 第一种方法在关闭日志以后以会有函数调用 toJson,会对性能造成影响,避免使用;
110 | 第二种方法在真正记录日志时才会调用 params 的 toString()方法,推荐使用。
111 | 第三种方法在真正记录日志时才会调用 toJson 方法,推荐使用。
112 |
113 | ## 实例基本约定
114 |
115 | 根据不同场景介绍日志的记录约定,约定是灵活的,在合理的情况下,可以适当不遵守,但是需要有比较好的理由。
116 |
117 | ### API 入参、返回值的记录
118 |
119 | - Rest API
120 |
121 | ```txt
122 | 一般而言,我们会在API入口处,将传入API的参数记录下来,在我们系统中,这里一般是Controller层。
123 | Controller层会完成API参数的校验、必要的参数转换、业务运行环境的准备(如获取当前请求用户)、调用具体service。
124 | ```
125 |
--------------------------------------------------------------------------------
/coding-guide/how-to-make-your-code-more-safely.md:
--------------------------------------------------------------------------------
1 | # 代码安全建设
2 |
3 | - 背景
4 | - 勒索病毒暴露出相关人员的安全意识薄弱,同时研发部的安全也要引起重视。本次会议主要讨论代码层面要关注哪些点
5 | - 最少暴露原则
6 | - 不该给前端的数据不应该暴露
7 | - 例子 1:国际机票订单详情显示到了前端
8 | - 例子 2:基础数据保险 保险成本暴露给了前端
9 | - 不该暴露的 API
10 | - 基础数据接口是否要拿到 token 才能访问
11 | - gateway 配置
12 | - 密码明文传递 不是问题 https 解决
13 | - 关键数据以服务端为准,三方数据都要做检查
14 | - 金钱有关信息要做校验
15 | - 计算不依靠前端
16 | - 供应商数据检查
17 | - 服务端安全
18 | - 接口
19 | - 鉴权,防止无权限的客户访问接口
20 | - API 防刷
21 | - 数据访问权限
22 | - 自己能访问自己数据
23 | - 管理员能访问下级数据
24 | - 同级之间不可互访问信息
25 | - SQL 注入防止
26 |
--------------------------------------------------------------------------------
/coding-guide/how-to-review-code.md:
--------------------------------------------------------------------------------
1 | # Code Review
2 |
3 | ## 1 基本原则
4 |
5 | - 代码审查是最有效率的质量改善工具。比各种测试都有效。
6 | - 代码审查能减少出现安全问题的可能性。
7 | - 审查者要像自己写代码一样,确认阅读和理解每一行语句。
8 | - 如果审查者和程序员不能达成一致,由团队其他人协调。
9 |
10 | ## 2 前置说明
11 |
12 | - 在讲如何 review code 之前,先简单说说怎么提 pr,这会让 review 变得更加容易。
13 |
14 | ### 2.1 pr 的原则
15 |
16 | - 尽可能做到一个 pr 只做一件事、或者一组类似的事,如果做了其他事一定要说明清楚。
17 | - 尽量将功能、缺陷修复、重构、优化分开处理,搅在一起会给 reviewer 带来很大的心智负担。
18 |
19 | ### 2.2 如何提 pr
20 |
21 | - commit:一个 pr 中的每个 commit 应该都是明确的,可以点开单独看的。
22 | - pr title:概括一下做了什么。
23 | - pr comment:
24 | - 一条条列举做了哪些事情。
25 | - 如果有需求链接、缺陷链接,请张贴出来。
26 | - 必要的话可以上传截图。
27 |
28 | ## 3 审查的颗粒度
29 |
30 | - 审查的代码量可以很小,但是最大不能超过一周代码的上限。
31 | - 对于小需求和缺陷修复,以可以验证或操作的功能为单位进行审查。
32 | - 如果对三天或以上的代码量审查,需要提前 24 小时找到审查者并告知可能的审查工作量。
33 |
34 | ## 4 检查事项
35 |
36 | ### 4.1 指导思想
37 |
38 | - 认真阅读和理解每一行代码,如同自己重写一遍。
39 | - 所有的建议和讨论都在 PR 上面保留。
40 |
41 | ### 4.2 可用性 review
42 |
43 | - 代码可以编译、可以 Merge。不可以就停止审查。
44 | - 如果有可验证的用户界面,先操作界面完成所需功能。不对就停止审查。
45 |
46 | ### 4.3 设计文档 review
47 |
48 | - 为提高审查效率,先理解高层的代码设计和实现功能。
49 | - 复杂的代码模块是否有设计文档,没有就停止审查。
50 | - 相关的需求或设计文档是否同步更新,没有就停止审查。
51 |
52 | ### 4.4 测试代码 review
53 |
54 | - 代码是否有关键模块的单元测试。
55 | - 核心流程是否有集成测试。
56 |
57 | ### 4.5 安全性 review
58 |
59 | #### 4.5.1 接口安全
60 |
61 | - 接口入参
62 | - 非表单填写的数据,不信任前端传值,后端自己从数据库取。
63 | - 接口返回
64 | - 接口不该给前台客户的数据不应该暴露,比如成本价格等。
65 | - 敏感字段脱敏。
66 | - 接口鉴权
67 | - 只能访问自己能看到的数据。
68 | - 高权限能访问低权限的数据,反之不可。
69 | - 不能横向越权。
70 |
71 | #### 4.5.2 数据库安全
72 |
73 | - 检查是否有 sql 注入的风险
74 | - 仔细检查 delete 相关的业务逻辑
75 |
76 | ### 4.6 发版 sql review
77 |
78 | - delete 语句好好检查,要有站得住脚的理由。
79 | - update 语句检查是否有类似全表更新这样的危险操作,同样要有站得住脚的理由。
80 | - update 语句 where 条件检查,看是否会造成锁表。
81 | - 建表语句需要指定 utf8 适用的字符集和比较集 DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_unicode_ci
82 | - 检查是否能灰度发版,判断方式比较复杂,参考:[如何实现灰度发版-数据库篇.md](./如何实现灰度发版-数据库篇.md)
83 |
--------------------------------------------------------------------------------
/coding-guide/how-to-write-commit-message.md:
--------------------------------------------------------------------------------
1 | # Write Good Commit Message
2 |
3 | 写出好的提交信息有助于提高代码可维护性。提交信息包含三个部分:标题(Subject),主体(Body)和相关 Issue。各个部分之间用空行隔开。参考了[write good git commit meesage](https://juffalow.com/other/write-good-git-commit-message).
4 |
5 | ## 提交信息建议
6 |
7 | ### 标题
8 |
9 | 好的标题应该回答这个问题: 如果生效,这个提交(commit)""
10 | 在空的地方应该填入动词加名词的一个简单句子。不超过 50 个字母。
11 |
12 | 好的例子:
13 |
14 | - 如果生效,这个提交(commit)“删除多余的文件”
15 | - 如果生效,这个提交(commit)“可以选择多个用户地址”
16 | - 如果生效,这个提交(commit)"fix bug when network is disconnected"
17 |
18 | 不好的例子
19 |
20 | - 如果生效,这个提交(commit)“fix bug"
21 | - 如果生效,这个提交(commit)"user group"
22 |
23 | #### 标题行的基本规范
24 | ```html
25 | ():
26 |
27 |
28 |
29 |