SUMMARY

(二) 二方库依赖

  1. 【强制】定义 GAV 遵从以下规则:
    1) GroupID 格式:com.{公司/BU }.业务线.[子业务线],最多 4 级。
    说明:{公司/BU} 例如:alibaba/taobao/tmall/aliexpress 等 BU 一级;子业务线可选。
    正例:com.taobao.jstorm 或 com.alibaba.dubbo.register

    2) ArtifactID 格式:产品线名-模块名。语义不重复不遗漏,先到中央仓库去查证一下。

    正例:dubbo-client / fastjson-api / jstorm-tool
    3) Version:详细规定参考下方。
  2. 【强制】二方库版本号命名方式:主版本号.次版本号.修订号
    1) 主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
    2) 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
    3) 修订号:保持完全兼容性,修复 BUG、新增次要功能特性等。
    说明:注意起始版本号必须为:_1.0.0_,而不是 0.0.1 正式发布的类库必须先去中央仓库进行查证,使版本号有延续性,正式版本号不允许覆盖升级。如当前版本:1.3.3,那么下一个合理的版本号:1.3.4 或 1.4.0 或 2.0.0
  3. 【强制】线上应用不要依赖 SNAPSHOT 版本(安全包除外)。

    说明:不依赖 SNAPSHOT 版本是保证应用发布的幂等性。另外,也可以加快编译时的打包构建。
  4. 【强制】二方库的新增或升级,保持除功能点之外的其它 jar 包仲裁结果不变。如果有改变,必须明确评估和验证,建议进行dependency:resolve前后信息比对,如果仲裁结果完全不一致,那么通过dependency:tree命令,找出差异点,进行<excludes>排除 jar 包。
  5. 【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象。
  6. 【强制】依赖于一个二方库群时,必须定义一个统一的版本变量,避免版本号不一致。

    说明:依赖 springframework-core,-context,-beans,它们都是同一个版本,可以定义一个变量来保存版本:${spring.version},定义依赖的时候,引用该版本。
  7. 【强制】禁止在子项目的 pom 依赖中出现相同的 GroupId,相同的 ArtifactId,但是不同的 Version。

    说明:在本地调试时会使用各子项目指定的版本号,但是合并成一个 war,只能有一个版本号出现在最后的 lib 目录中。可能出现线下调试是正确的,发布到线上却出故障的问题。
  8. 【推荐】所有 pom 文件中的依赖声明放在<dependencies>语句块中,所有版本仲裁放在<dependencyManagement>语句块中。

    说明<dependencyManagement>里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖,version 和 scope 都读取自父 pom。而<dependencies>所有声明在主 pom 的<dependencies>里的依赖都会自动引入,并默认被所有的子项目继承。
  9. 【推荐】二方库不要有配置项,最低限度不要再增加配置项。
  10. 【参考】为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:

    1)精简可控原则。移除一切不必要的 API 和依赖,只包含 Service API、必要的领域模型对象、Utils 类、常量、枚举等。如果依赖其它二方库,尽量是 provided 引入,让二方库使用者去依赖具体版本号;无 log 具体实现,只依赖日志框架。

    2)稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到。除非用户主动升级版本,否则公共二方库的行为不应该发生变化。

SUMMARY

五、MySQL 数据库

(一) 建表规约

  1. 【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint( 1 表示是,0 表示否)。

    说明:任何字段如果为非负数,必须是unsigned

    正例:表达逻辑删除的字段名is_deleted,1 表示删除,0 表示未删除。
  2. 【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
    说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
    正例:aliyun_admin,rdc_config,level3_name
    反例:AliyunAdmin,rdcConfig,level_3_name
  3. 【强制】表名不使用复数名词。

    说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。
  4. 【强制】禁用保留字,如descrangematchdelayed等,请参考 MySQL 官方保留字。
  5. 【强制】主键索引名为 pk字段名;唯一索引名为 uk字段名;普通索引名则为 idx字段名。

    说明:pk
    即 primary key;uk* 即 unique key;idx* 即 index 的简称。
  6. 【强制】小数类型为 decimal,禁止使用 float 和 double。

    说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
  7. 【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
  8. 【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
  9. 【强制】表必备三字段:id, gmt_create, gmt_modified。

    说明:其中 id 必为主键,类型为 unsigned bigint、单表时自增、步长为 1。gmt_create, gmt_modified 的类型均为 datetime 类型,前者现在时表示主动创建,后者过去分词表示被动更新。
  10. 【推荐】表的命名最好是加上“业务名称_表的作用”。

    正例:alipay_task / force_project / trade_config
  11. 【推荐】库名与应用名称尽量一致。
  12. 【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
  13. 【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
    1)不是频繁修改的字段。
    2)不是 varchar 超长字段,更不能是 text 字段。

    正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。
  14. 【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
    说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
  15. 【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
    正例:如下表,其中无符号值可以避免误存负数,且扩大了表示范围。
对象 年龄区间 类型 字节
150 岁之内 unsigned tinyint 1
数百岁 unsigned smallint 2
恐龙化石 数千万岁 unsigned int 4
太阳 约 50 亿年 unsigned bigint 8

SUMMARY

(一) 异常处理

  1. 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
    说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,不得不通过 catch NumberFormatException 来实现。
    正例
    if (obj != null) {...} 

    反例
    try { obj.method() } catch (NullPointerException e) {…}
  2. 【强制】异常不要用来做流程控制,条件控制。

    说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
  3. 【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。

    说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。

    正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
  4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
  5. 【强制】有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
  6. 【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。

    说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
  7. 【强制】不要在 finally 块中使用 return。

    说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
  8. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。

    说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
  9. 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分

    说明什么情况下会返回 null 值。
    说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
  10. 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
    1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
    反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
    2) 数据库的查询结果可能为 null。
    3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
    4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
    5) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
    6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。

    正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
  11. 【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
  12. 【参考】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”。

    说明:关于 RPC 方法返回方式使用 Result 方式的理由:

    1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
    2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
  13. 【参考】避免出现重复的代码(Don’t Repeat Yourself),即 DRY 原则。

    说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
    正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
    private boolean checkParam(DTO dto) {...}

SUMMARY

六、工程结构

(一) 应用分层

  1. 【推荐】图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于 Web 层,也可以直接依赖于 Service 层,依此类推:
    应用分层
  • 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行网关安全控制、流量控制等。
  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。
  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层:相对具体的业务逻辑服务层。
  • Manager 层:通用业务处理层,它有如下特征:

    1) 对第三方平台封装的层,预处理返回结果及转化异常信息;

    2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;

    3) 与 DAO 层交互,对多个 DAO 的组合复用。
  • DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
  • 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。
  1. 【参考】(分层异常处理规约)在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行 catch,使用 catch(Exception e)方式,并 throw new DAOException(e),不需要打印日志,因为日志在 Manager/Service 层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。如果 Manager 层与 Service 同机部署,日志方式与 DAO 层处理一致,如果是单独部署,则采用与 Service 一致的处理方式。Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该跳转到友好错误页面,加上用户容易理解的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。

  2. 【参考】分层领域模型规约:

  • DO(Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
  • BO(Business Object):业务对象。由 Service 层输出的封装业务逻辑的对象。
  • AO(Application Object):应用对象。在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
  • VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。

来源: An alternative to If/Else

多少算太多

  • 很多开发人员 if/else 很容易陷入陷阱,不是因为其他解决方案的复杂性,而是因为它遵循这样一种自然语言模式:
    if 某事做这个,else 做另外的。

三元表达式

  • 只是换了一种方式
1
2
3
const greaterThanFive = 8 > 5 ? 'yep' : 'nope';
console.log(greaterThanFive); // 'yep'
condition ? isTruthy : isFalsy;

If/Else 的替代方案

  • 场景:从用户输入中获取颜色,并需要将它们转换为一些预设的颜色代码来匹配,以便我们可以更改背景颜色。因此,我们将检查颜色名称字符串,并在匹配时设置颜色代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const setBackgroundColor = (colorName) => {
let colorCode = '';
if (colorName === 'blue') {
colorCode = '#2196F3';
} else if (colorName === 'green') {
colorCode = '#4CAF50';
} else if (colorName === 'orange') {
colorCode = '#FF9800';
} else if (colorName === 'pink') {
colorCode = '#E91E63';
} else {
colorCode = '#F44336';
}
document.body.style.backgroundColor = colorCode;
};

改成 Switch

  • 使用 Switch 后还是有重复的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const setBackgroundColor = (colorName) => {
let colorCode = '';
switch (colorName) {
case 'blue':
colorCode = '#2196F3';
break;
case 'green':
colorCode = '#4CAF50';
break;
case 'orange':
colorCode = '#FF9800';
break;
case 'pink':
colorCode = '#E91E63';
break;
default:
colorCode = '#f44336';
}
document.body.style.backgroundColor = colorCode;
};

转换成 key-value 模式获取值

  • 从常量里面获取对应的值
  • 这个并没有减少代码的总行数,但是提高了可维护性、易读性,并且实际上通过仅对默认回退进行一次逻辑检查来降低复杂性。
1
2
3
4
5
6
7
8
9
10
11
const colorCodes = {
blue: '#2196F3',
green: '#4CAF50',
orange: '#FF9800',
pink: '#E91E63',
default: '#F44336',
};

const setBackgroundColor = (colorName) => {
document.body.style.backgroundColor = colorCodes[colorName] ? colorCodes[colorName] : colorCodes['default'];
};

更复杂的数字判断逻辑

  • 场景:我们需要将成绩转换为成绩字母等级。
1
2
3
4
5
6
7
8
9
10
11
12
13
const getLetterGrade = (gradeAsPercent) => {
if (gradeAsPercent >= 90) {
return 'A';
} else if (gradeAsPercent >= 80) {
return 'B';
} else if (gradeAsPercent >= 70) {
return 'C';
} else if (gradeAsPercent >= 60) {
return 'D';
} else {
return 'F';
}
};
  • 将数据提取到一个数组中(顺序从大到小)并将每个等级的可能性表示为一个对象
  • 只需>=对对象进行一次比较,并找到数组中第一个匹配的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
const gradeChart = [
{ minpercent: 90, letter: 'A' },
{ minpercent: 80, letter: 'B' },
{ minpercent: 70, letter: 'C' },
{ minpercent: 60, letter: 'D' },
{ minpercent: 0, letter: 'F' },
];

const getLetterGrade = (gradeAsPercent) => {
const grade = gradeChart.find((grade) => gradeAsPercent >= grade.minpercent);

return grade.letter;
};

镜像加速设置

也可命令行更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# nas 地址
ssh user@192.168.1.100

# 切换root
sudo -i

# 已下载镜像列表
docker images

# 删除镜像
docker rmi {id}

# 运行中
docker ps

# 在mac或者其他linux上查看docker registry 地址
dig @114.114.114.114 registry-1.docker.io

# ; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.9 <<>> @114.114.114.114 registry-1.docker.io
# ; (1 server found)
# ;; global options: +cmd
# ;; Got answer:
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19255
# ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
#
# ;; OPT PSEUDOSECTION:
# ; EDNS: version: 0, flags:; udp: 512
# ;; QUESTION SECTION:
# ;registry-1.docker.io. IN A

# ;; ANSWER SECTION:
# registry-1.docker.io. 73 IN A 3.216.34.172
# registry-1.docker.io. 73 IN A 34.205.13.154
# registry-1.docker.io. 73 IN A 44.205.64.79

# ;; Query time: 10 msec
# ;; SERVER: 114.114.114.114#53(114.114.114.114)
# ;; WHEN: Sat Oct 15 11:49:25 CST 2022
# ;; MSG SIZE rcvd: 97

# 在nas上配置host文件
vim /etc/hosts
# 3.216.34.172 地址配置到host文件

升级步骤

第 1 步,下载最新镜像

第 2 步,重命名原容器

  • 修改端口
  • 做好备份

第 3 步,选中要更新的容器

  • 设置
    • 复制配置
      • 端口设置无法复制
  • 设置原端口配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import com.google.common.collect.Lists;
import java.util.concurrent.Callable;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;

/**
* @author demo
* @date 2022-07-23
*/
@Slf4j
class SortTest {

private static final List<Bean> list = Lists.newArrayList(
new Bean("1", "abc"),
new Bean("3", "zzz"),
new Bean("2", "ddd"),
new Bean("2", "aaa"),
new Bean("8", "ccc"),
new Bean("6", "bbc"));

@Test
void sort1() {
//noinspection Java8ListSort
Collections.sort(list, new Comparator<>() {
@Override
public int compare(Bean a, Bean b) {
return a.getId().compareTo(b.getId());
}
});
for (Bean bean : list) {
log.debug("id={}", bean.getId());
}
assertTrue(true);
}

@Test
void sort2() {
// list.sort((Bean a, Bean b) -> a.getId().compareTo(b.getId()));
list.sort(Comparator.comparing(Bean::getId));
list.forEach(bean -> log.debug("id={}", bean.getId()));
assertTrue(true);
}

@Test
void sort2_1() {
list.sort(Comparator.comparing(Bean::getId).reversed());
list.forEach(bean -> log.debug("id={}", bean.getId()));
assertTrue(true);
}

@Test
void sort3() {
list.sort(Bean::compareById);
list.forEach(bean -> log.debug("id={}", bean.getId()));
assertTrue(true);
}

@Test
void sort3_1() {
//noinspection Java8ListSort
Collections.sort(list, Bean::compareById);
list.forEach(bean -> log.debug("id={},name={}", bean.getId(), bean.getName()));
assertTrue(true);
}

@Test
void sort4() {
list.sort(Bean.getInstance()::compare);
list.forEach(bean -> log.debug("id={}", bean.getId()));
assertTrue(true);
}

@Test
void sort5() {
list.sort(Comparator.comparing(Bean::getId).thenComparing(Bean::getName));
list.forEach(bean -> log.debug("id={},name={}", bean.getId(), bean.getName()));
assertTrue(true);
}

@Test
void sort6() {
List<Bean> list1 = list.stream().sorted(Comparator.comparing(Bean::getId)).toList();
list1.forEach(bean -> log.debug("id={},name={}", bean.getId(), bean.getName()));
assertTrue(true);
}

@Test
void sort7() {
List<Bean> list1 = list.parallelStream().sorted(Comparator.comparing(Bean::getId)).toList();
list1.forEach(bean -> log.debug("id={},name={}", bean.getId(), bean.getName()));
assertTrue(true);
}

/**
* 统计程序执行时间
*
* @throws InterruptedException 异常
*/
@Test
void stopWatch() throws InterruptedException {
StopWatch sw1 = new StopWatch();
sw1.start();
StopWatch sw2 = StopWatch.create();
sw2.start();
StopWatch sw3 = StopWatch.createStarted();

// 优雅的sleep
TimeUnit.SECONDS.sleep(1L);
await().atMost(2L, TimeUnit.SECONDS).until(didTheThing());
log.info("time={}", sw3.getTime());
// TimeUnit.MILLISECONDS.sleep(200L);
await().atMost(400L, TimeUnit.MILLISECONDS).until(didTheThing());
log.info("time={}", sw3.getTime());
// 暂停
sw3.suspend();
// TimeUnit.MILLISECONDS.sleep(500L);
await().atMost(500L, TimeUnit.MILLISECONDS).until(didTheThing());
log.info("time={}", sw3.getTime());
// 恢复
sw3.resume();
// TimeUnit.MILLISECONDS.sleep(500L);
await().atMost(500L, TimeUnit.MILLISECONDS).until(didTheThing());
log.info("time={}", sw3.getTime());
assertTrue(true);
}

@Test
void stopWatchSpring() throws InterruptedException {
org.springframework.util.StopWatch sw1 = new org.springframework.util.StopWatch("test task times");

sw1.start("task a");
// TimeUnit.SECONDS.sleep(1L);
await().atMost(2L, TimeUnit.SECONDS).until(didTheThing());
sw1.stop();
log.info("time={}", sw1.getLastTaskTimeMillis());

sw1.start("task b");
// TimeUnit.MILLISECONDS.sleep(500L);
await().atMost(500L, TimeUnit.MILLISECONDS).until(didTheThing());
sw1.stop();
log.info("time={}", sw1.getLastTaskTimeMillis());

log.info("count={}, total time={}", sw1.getTaskCount(), sw1.getTotalTimeMillis());
log.debug(sw1.prettyPrint());
assertTrue(true);
}


private Callable<Boolean> didTheThing() {
return () -> {
// check the condition that must be fulfilled...
return true;
};
}


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
static class Bean {

private static final Bean BEAN = new Bean();
private String id;
private String name;

public static Bean getInstance() {
return BEAN;
}

public static int compareById(Bean a, Bean b) {
return a.getId().compareTo(b.getId());
}

public int compare(Bean a, Bean b) {
return a.getId().compareTo(b.getId());
}
}
}

来源: 你见过哪些目瞪口呆的 Java 代码技巧?

导语

自从毕业后,今年已经是我工作的第 8 个年头了,我甚至都快忘记了到底是哪年毕业的。

从出来,本人一直在做 Java 相关的工作,现在终于有时间坐下来,写一篇关于 Java 写法的一篇文章,来探讨一下如果你真的是一个 Java 程序员,那你真的会写 Java 吗?

笔者是一个务实的程序员,故本文绝非扯淡文章,文中内容都是干货,望读者看后,能有所收获。

本文不是一个吹嘘的文章,不会讲很多高深的架构,相反,会讲解很多基础的问题和写法问题,如果读者自认为基础问题和写法问题都是不是问题,那请忽略这篇文章,节省出时间去做一些有意义的事情。

开发工具

不知道有多少“老”程序员还在使用 Eclipse,这些程序员们要不就是因循守旧,要不就是根本就不知道其他好的开发工具的存在,Eclipse 吃内存卡顿的现象以及各种偶然莫名异常的出现,都告知我们是时候寻找新的开发工具了。

更换 IDE

根本就不想多解释要换什么样的 IDE,如果你想成为一个优秀的 Java 程序员,请更换 IntelliJ IDEA。使用 IDEA 的好处,请搜索谷歌。

别告诉我快捷键不好用

更换 IDE 不在我本文的重点内容中,所以不想用太多的篇幅去写为什么更换IDE。在这里,我只能告诉你,更换 IDE 只为了更好、更快的写好 Java 代码。原因略。

别告诉我快捷键不好用,请尝试新事物。

bean

bean 使我们使用最多的模型之一,我将以大篇幅去讲解 bean,希望读者好好体会。

domain 包名

根据很多 Java 程序员的“经验”来看,一个数据库表则对应着一个 domain 对象,所以很多程序员在写代码时,包名则使用:com.xxx.domain ,这样写好像已经成为了行业的一种约束,数据库映射对象就应该是 domain。

但是你错了,domain 是一个领域对象,往往我们再做传统 Java 软件 Web 开发中,这些 domain 都是贫血模型,是没有行为的,或是没有足够的领域模型的行为的。

所以,以这个理论来讲,这些 domain 都应该是一个普通的 entity 对象,并非领域对象,所以请把包名改为:com.xxx.entity。

如果你还不理解我说的话,请看一下 Vaughn Vernon 出的一本叫做《IMPLEMENTING DOMAIN-DRIVEN DESIGN》(实现领域驱动设计)这本书,书中讲解了贫血模型与领域模型的区别,相信你会受益匪浅。

DTO

数据传输我们应该使用 DTO 对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端 API 设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or output),这些对象成为 DTO 对象。

请注意!这种理解是错误的,只要是用于网络传输的对象,我们都认为他们可以当做是 DTO 对象,比如电商平台中,用户进行下单,下单后的数据,订单会发到 OMS 或者 ERP 系统,这些对接的返回值以及入参也叫 DTO 对象。

我们约定某对象如果是 DTO 对象,就将名称改为 XXDTO,比如订单下发 OMS:OMSOrderInputDTO。

DTO 转化

正如我们所知,DTO 为系统与外界交互的模型对象,那么肯定会有一个步骤是将 DTO 对象转化为 BO 对象或者是普通的 entity 对象,让 service 层去处理。

场景

比如添加会员操作,由于用于演示,我只考虑用户的一些简单数据,当后台管理员点击添加用户时,只需要传过来用户的姓名和年龄就可以了,后端接受到数据后,将添加创建时间和更新时间和默认密码三个字段,然后保存数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/v1/api/user")
@RestController
public class UserApi {

@Autowired
private UserService userService;

@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());

return userService.addUser(user);
}
}
我们只关注一下上述代码中的转化代码,其他内容请忽略:

User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());

请使用工具

上边的代码,从逻辑上讲,是没有问题的,只是这种写法让我很厌烦,例子中只有两个字段,如果有 20 个字段,我们要如何做呢?一个一个进行 set 数据吗?

当然,如果你这么做了,肯定不会有什么问题,但是,这肯定不是一个最优的做法。网上有很多工具,支持浅拷贝或深拷贝的 Utils。

举个例子,我们可以使用 org.springframework.beans.BeanUtils#copyProperties 对代码进行重构和优化:

1
2
3
4
5
6
7
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);

return userService.addUser(user);
}

BeanUtils.copyProperties 是一个浅拷贝方法,复制属性时,我们只需要把 DTO 对象和要转化的对象两个的属性值设置为一样的名称,并且保证一样的类型就可以了。

如果你在做 DTO 转化的时候一直使用 set 进行属性赋值,那么请尝试这种方式简化代码,让代码更加清晰!另外,最新面试题整理好了,点击Java面试库小程序在线刷题。

转化的语义

上边的转化过程,读者看后肯定觉得优雅很多,但是我们再写 Java 代码时,更多的需要考虑语义的操作,再看上边的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
虽然这段代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要提现一个转化过程才好,所以代码改成如下:

@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = convertFor(userInputDTO);

return userService.addUser(user);
}

private User convertFor(UserInputDTO userInputDTO){

User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
这是一个更好的语义写法,虽然他麻烦了些,但是可读性大大增加了,在写代码时,我们应该尽量把语义层次差不多的放到一个方法中,比如:

User user = convertFor(userInputDTO);
return userService.addUser(user);

这两段代码都没有暴露实现,都是在讲如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。

如上所述,是一种重构方式,读者可以参考 Martin Fowler 的《Refactoring Imporving the Design of Existing Code》(重构改善既有代码的设计)这本书中的 Extract Method 重构方式。

抽象接口定义

当实际工作中,完成了几个 API 的 DTO 转化时,我们会发现,这样的操作有很多很多,那么应该定义好一个接口,让所有这样的操作都有规则的进行。

如果接口被定义以后,那么 convertFor 这个方法的语义将产生变化,它将是一个实现类。

看一下抽象后的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface DTOConvert<S,T> {
T convert(S s);
}
虽然这个接口很简单,但是这里告诉我们一个事情,要去使用泛型,如果你是一个优秀的 Java 程序员,请为你想做的抽象接口,做好泛型吧。

我们再来看接口实现:
public class UserInputDTOConvert implements DTOConvert {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
我们这样重构后,我们发现现在的代码是如此的简洁,并且那么的规范:

@RequestMapping("/v1/api/user")
@RestController
public class UserApi {

@Autowired
private UserService userService;

@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);

return userService.addUser(user);
}
}

review code

如果你是一个优秀的 Java 程序员,我相信你应该和我一样,已经数次重复 review 过自己的代码很多次了。

我们再看这个保存用户的例子,你将发现,API 中返回值是有些问题的,问题就在于不应该直接返回 User 实体,因为如果这样的话,就暴露了太多实体相关的信息,这样的返回值是不安全的。

所以我们更应该返回一个 DTO 对象,我们可称它为 UserOutputDTO:

1
2
3
4
5
6
7
@PostMapping
public UserOutputDTO addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);
UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult);
return result;
}

这样你的 API 才更健全。

不知道在看完这段代码之后,读者有是否发现还有其他问题的存在,作为一个优秀的 Java 程序员,请看一下这段我们刚刚抽象完的代码:

User user = new UserInputDTOConvert().convert(userInputDTO);
你会发现,new 这样一个 DTO 转化对象是没有必要的,而且每一个转化对象都是由在遇到 DTO 转化的时候才会出现,那我们应该考虑一下,是否可以将这个类和 DTO 进行聚合呢

看一下我的聚合结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class UserInputDTO {
private String username;
private int age;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}


public User convertToUser(){
UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
User convert = userInputDTOConvert.convert(this);
return convert;
}

private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}

}
然后 API 中的转化则由:

User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);
变成了:

User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);
我们再 DTO 对象中添加了转化的行为,我相信这样的操作可以让代码的可读性变得更强,并且是符合语义的。

再查工具类

再来看 DTO 内部转化的代码,它实现了我们自己定义的 DTOConvert 接口,但是这样真的就没有问题,不需要再思考了吗?

我觉得并不是,对于 Convert 这种转化语义来讲,很多工具类中都有这样的定义,这中 Convert 并不是业务级别上的接口定义,它只是用于普通 bean 之间转化属性值的普通意义上的接口定义,所以我们应该更多的去读其他含有 Convert 转化语义的代码。

我仔细阅读了一下 GUAVA 的源码,发现了 com.google.common.base.Convert 这样的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public abstract class Converter<A, B> implements Function<A, B> {
protected abstract B doForward(A a);
protected abstract A doBackward(B b);
//其他略
}
从源码可以了解到,GUAVA 中的 Convert 可以完成正向转化和逆向转化,继续修改我们 DTO 中转化的这段代码:

private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
修改后:

private static class UserInputDTOConvert extends Converter<UserInputDTO, User> {
@Override
protected User doForward(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}

@Override
protected UserInputDTO doBackward(User user) {
UserInputDTO userInputDTO = new UserInputDTO();
BeanUtils.copyProperties(user,userInputDTO);
return userInputDTO;
}
}

看了这部分代码以后,你可能会问,那逆向转化会有什么用呢?其实我们有很多小的业务需求中,入参和出参是一样的,那么我们变可以轻松的进行转化,我将上边所提到的 UserInputDTO 和 UserOutputDTO 都转成 UserDTO 展示给大家。

DTO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class UserDTO {
private String username;
private int age;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}


public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}

public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}

private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}

@Override
protected UserDTO doBackward(User user) {
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
return userDTO;
}
}

}
API:

@PostMapping
public UserDTO addUser(UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}

当然,上述只是表明了转化方向的正向或逆向,很多业务需求的出参和入参的 DTO 对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}

@Override
protected UserDTO doBackward(User user) {
throw new AssertionError("不支持逆向转化方法!");
}
}

看一下 doBackward 方法,直接抛出了一个断言异常,而不是业务异常,这段代码告诉代码的调用者,这个方法不是准你调用的,如果你调用,我就“断言”你调用错误了。

关于异常处理的更详细介绍,可以参考这篇文章:优雅地处理你的 Java 异常吧,不然后果很严重!

bean 的验证

如果你认为我上边写的那个添加用户 API 写的已经非常完美了,那只能说明你还不是一个优秀的程序员。我们应该保证任何数据的入参到方法体内都是合法的。

为什么要验证

很多人会告诉我,如果这些 API 是提供给前端进行调用的,前端都会进行验证啊,你为什还要验证?

其实答案是这样的,我从不相信任何调用我 API 或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如 Charles 进行抓包),直接将数据传入到我的 API,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!

“对于脏数据的产生一定是致命”,这句话希望大家牢记在心,再小的脏数据也有可能让你找几个通宵!

jsr 303 验证

hibernate 提供的 jsr 303 实现,我觉得目前仍然是很优秀的,具体如何使用,我不想讲,因为谷歌上你可以搜索出很多答案!

再以上班的 API 实例进行说明,我们现在对 DTO 数据进行检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class UserDTO {
@NotNull
private String username;
@NotNull
private int age;
//其他代码略
}
API 验证:

@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
我们需要将验证结果传给前端,这种异常应该转化为一个 api 异常(带有错误码的异常)。

@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
checkDTOParams(bindingResult);

User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
private void checkDTOParams(BindingResult bindingResult){
if(bindingResult.hasErrors()){
//throw new 带验证码的验证错误异常
}
}

BindingResult 是 Spring MVC 验证 DTO 后的一个结果集,可以参考 spring 官方文档:

https://spring.io/
检查参数后,可以抛出一个“带验证码的验证错误异常”。

拥抱 lombok

上边的 DTO 代码,已经让我看的很累了,我相信读者也是一样,看到那么多的 Getter 和 Setter 方法,太烦躁了,那时候有什么方法可以简化这些呢。

请拥抱 lombok,它会帮助我们解决一些让我们很烦躁的问题。详细看以看这篇教程:https://www.javastack.cn/article/2019/lombok-introduce/

去掉 Setter 和 Getter

其实这个标题,我不太想说,因为网上太多,但是因为很多人告诉我,他们根本就不知道 lombok 的存在,所以为了让读者更好的学习,我愿意写这样一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Setter
@Getter
public class UserDTO {
@NotNull
private String username;
@NotNull
private int age;

public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}

public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}

private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}

@Override
protected UserDTO doBackward(User user) {
throw new AssertionError("不支持逆向转化方法!");
}
}
}

看到了吧,烦人的 Getter 和 Setter 方法已经去掉了。

但是上边的例子根本不足以体现 lombok 的强大。我希望写一些网上很难查到,或者很少人进行说明的 lombok 的使用以及在使用时程序语义上的说明。

比如:@Data,@AllArgsConstructor,@NoArgsConstructor..这些我就不进行一一说明了,请大家自行查询资料。

bean 中的链式风格

什么是链式风格?我来举个例子,看下面这个 Student 的 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Student {
private String name;
private int age;

public String getName() {
return name;
}

public Student setName(String name) {
this.name = name;
return this;
}

public int getAge() {
return age;
}

public Student setAge(int age) {
return this;
}
}

仔细看一下 set 方法,这样的设置便是 chain 的 style,调用的时候,可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
Student student = new Student()
.setAge(24)
.setName("zs");
相信合理使用这样的链式代码,会更多的程序带来很好的可读性,那看一下如果使用 lombok 进行改善呢,请使用 @Accessors(chain = true),看如下代码:

@Accessors(chain = true)
@Setter
@Getter
public class Student {
private String name;
private int age;
}

这样就完成了一个对于 bean 来讲很友好的链式操作。

静态构造方法

静态构造方法的语义和简化程度真的高于直接去 new 一个对象。比如 new 一个 List 对象,过去的使用是这样的:

List list = new ArrayList<>();
看一下 guava 中的创建方式:

List list = Lists.newArrayList();
Lists 命名是一种约定(俗话说:约定优于配置),它是指 Lists 是 List 这个类的一个工具类,那么使用 List 的工具类去产生 List,这样的语义是不是要比直接 new 一个子类来的更直接一些呢,答案是肯定的。

再比如如果有一个工具类叫做 Maps,那你是否想到了创建 Map 的方法呢:

HashMap<String, String> objectObjectHashMap = Maps.newHashMap();
好了,如果你理解了我说的语义,那么,你已经向成为 Java 程序员更近了一步了。

再回过头来看刚刚的 Student,很多时候,我们去写 Student 这个 bean 的时候,他会有一些必输字段。

比如 Student 中的 name 字段,一般处理的方式是将 name 字段包装成一个构造方法,只有传入 name 这样的构造方法,才能创建一个 Student 对象。

接上上边的静态构造方法和必传参数的构造方法,使用 lombok 将更改成如下写法(@RequiredArgsConstructor 和 @NonNull):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "ofName")
public class Student {
@NonNull private String name;
private int age;
}
测试代码:

Student student = Student.ofName("zs");
这样构建出的 bean 语义是否要比直接 new 一个含参的构造方法(包含 name 的构造方法)要好很多。

当然,看过很多源码以后,我想相信将静态构造方法 ofName 换成 of 会先的更加简洁:

@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Student {
@NonNull private String name;
private int age;
}
测试代码:

Student student = Student.of("zs");
当然他仍然是支持链式调用的:

Student student = Student.of("zs").setAge(24);

这样来写代码,真的很简洁,并且可读性很强。另外,关注公众号Java核心技术,回复:手册,可以获取阿里最新的Java开发手册。

使用 builder

Builder 模式我不想再多解释了,读者可以看一下《Head First》(设计模式) 的建造者模式。23 种设计模式实战(很全)分享给你,好好看下吧。

今天其实要说的是一种变种的 builder 模式,那就是构建 bean 的 builder 模式,其实主要的思想是带着大家一起看一下 lombok 给我们带来了什么。

看一下 Student 这个类的原始 builder 状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Student {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public static Builder builder(){
return new Builder();
}
public static class Builder{
private String name;
private int age;
public Builder name(String name){
this.name = name;
return this;
}

public Builder age(int age){
this.age = age;
return this;
}

public Student build(){
Student student = new Student();
student.setAge(age);
student.setName(name);
return student;
}
}
}

调用方式:

1
2
3
4
5
6
7
8
9
Student student = Student.builder().name("zs").age(24).build();
// 这样的 builder 代码,让我是在恶心难受,于是我打算用 lombok 重构这段代码:
@Builder
public class Student {
private String name;
private int age;
}
// 调用方式:
Student student = Student.builder().name("zs").age(24).build();

代理模式

正如我们所知的,在程序中调用 rest 接口是一个常见的行为动作,如果你和我一样使用过 spring 的 RestTemplate,我相信你会我和一样,对他抛出的非 http 状态码异常深恶痛绝。

所以我们考虑将 RestTemplate 最为底层包装器进行包装器模式的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public abstract class FilterRestTemplate implements RestOperations {
protected volatile RestTemplate restTemplate;

protected FilterRestTemplate(RestTemplate restTemplate){
this.restTemplate = restTemplate;
}
// 实现RestOperations所有的接口
}
// 然后再由扩展类对 FilterRestTemplate 进行包装扩展:

public class ExtractRestTemplate extends FilterRestTemplate {
private RestTemplate restTemplate;
public ExtractRestTemplate(RestTemplate restTemplate) {
super(restTemplate);
this.restTemplate = restTemplate;
}

public <T> RestResponseDTO<T> postForEntityWithNoException(String url, Object request, Class<T> responseType, Object... uriVariables)
throws RestClientException {
RestResponseDTO<T> restResponseDTO = new RestResponseDTO<T>();
ResponseEntity<T> tResponseEntity;
try {
tResponseEntity = restTemplate.postForEntity(url, request, responseType, uriVariables);
restResponseDTO.setData(tResponseEntity.getBody());
restResponseDTO.setMessage(tResponseEntity.getStatusCode().name());
restResponseDTO.setStatusCode(tResponseEntity.getStatusCodeValue());
}catch (Exception e){
restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR);
restResponseDTO.setMessage(e.getMessage());
restResponseDTO.setData(null);
}
return restResponseDTO;
}
}

包装器 ExtractRestTemplate 很完美的更改了异常抛出的行为,让程序更具有容错性。

在这里我们不考虑 ExtractRestTemplate 完成的功能,让我们把焦点放在 FilterRestTemplate 上,“实现 RestOperations 所有的接口”。

这个操作绝对不是一时半会可以写完的,当时在重构之前我几乎写了半个小时,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class FilterRestTemplate implements RestOperations {

protected volatile RestTemplate restTemplate;

protected FilterRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

@Override
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVariables);
}

@Override
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVariables);
}

@Override
public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException {
return restTemplate.getForObject(url,responseType);
}

@Override
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
return restTemplate.getForEntity(url,responseType,uriVariables);
}
//其他实现代码略。。。
}

我相信你看了以上代码,你会和我一样觉得恶心反胃,后来我用 lombok 提供的代理注解优化了我的代码(@Delegate):

1
2
3
4
5
@AllArgsConstructor
public abstract class FilterRestTemplate implements RestOperations {
@Delegate
protected volatile RestTemplate restTemplate;
}

这几行代码完全替代上述那些冗长的代码。是不是很简洁,做一个拥抱 lombok 的程序员吧。

重构

项目需求

项目开发阶段,有一个关于下单发货的需求:如果今天下午 3 点前进行下单,那么发货时间是明天,如果今天下午 3 点后进行下单,那么发货时间是后天,如果被确定的时间是周日,那么在此时间上再加 1 天为发货时间。

思考与重构

我相信这个需求看似很简单,无论怎么写都可以完成。

很多人可能看到这个需求,就动手开始写 Calendar 或 Date 进行计算,从而完成需求。

而我给的建议是,仔细考虑如何写代码,然后再去写,不是说所有的时间操作都用 Calendar 或 Date 去解决,一定要看场景。

对于时间的计算我们要考虑 joda-time 这种类似的成熟时间计算框架来写代码,它会让代码更加简洁和易读。

请读者先考虑这个需求如何用 Java 代码完成,或先写一个你觉得完成这个代码的思路,再来看我下边的代码,这样,你的收获会更多一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
final DateTime DISTRIBUTION_TIME_SPLIT_TIME = new DateTime().withTime(15,0,0,0);
private Date calculateDistributionTimeByOrderCreateTime(Date orderCreateTime){
DateTime orderCreateDateTime = new DateTime(orderCreateTime);
Date tomorrow = orderCreateDateTime.plusDays(1).toDate();
Date theDayAfterTomorrow = orderCreateDateTime.plusDays(2).toDate();
return orderCreateDateTime.isAfter(DISTRIBUTION_TIME_SPLIT_TIME) ? wrapDistributionTime(theDayAfterTomorrow) : wrapDistributionTime(tomorrow);
}
private Date wrapDistributionTime(Date distributionTime){
DateTime currentDistributionDateTime = new DateTime(distributionTime);
DateTime plusOneDay = currentDistributionDateTime.plusDays(1);
boolean isSunday = (DateTimeConstants.SUNDAY == currentDistributionDateTime.getDayOfWeek());
return isSunday ? plusOneDay.toDate() : currentDistributionDateTime.toDate() ;
}

读这段代码的时候,你会发现,我将判断和有可能出现的不同结果都当做一个变量,最终做一个三目运算符的方式进行返回。

这样的优雅和可读性显而易见,当然这样的代码不是一蹴而就的,我优化了 3 遍产生的以上代码。读者可根据自己的代码和我写的代码进行对比。

提高方法

如果你做了 3 年+的程序员,我相信像如上这样的需求,你很轻松就能完成,但是如果你想做一个会写 Java 的程序员,就好好的思考和重构代码吧。

写代码就如同写字一样,同样的字,大家都会写,但是写出来是否好看就不一定了。如果想把程序写好,就要不断的思考和重构,敢于尝试,敢于创新,不要因循守旧,一定要做一个优秀的 Java 程序员。

提高代码水平最好的方法就是有条理的重构!(注意:是有条理的重构)

设计模式

设计模式就是工具,而不是提现你是否是高水平程序员的一个指标。设计模式最新面试题整理好了,点击Java面试库小程序在线刷题。

我经常会看到某一个程序员兴奋的大喊,哪个程序哪个点我用到了设计模式,写的多么多么优秀,多么多么好。我仔细去翻阅的时候,却发现有很多是过度设计的。

业务驱动技术 or 技术驱动业务

业务驱动技术 or 技术驱动业务 ?其实这是一个一直在争论的话题,但是很多人不这么认为,我觉得就是大家不愿意承认罢了。我来和大家大概分析一下作为一个 Java 程序员,我们应该如何判断自己所处于的位置。

业务驱动技术:如果你所在的项目是一个收益很小或者甚至没有收益的项目,请不要搞其他创新的东西,不要驱动业务要如何如何做,而是要熟知业务现在的痛点是什么?如何才能帮助业务盈利或者让项目更好,更顺利的进行。

技术驱动业务:如果你所在的项目是一个很牛的项目,比如淘宝这类的项目,我可以在满足业务需求的情况下,和业务沟通,使用什么样的技术能更好的帮助业务创造收益。

比如说下单的时候要进队列,可能几分钟之后订单状态才能处理完成,但是会让用户有更流畅的体验,赚取更多的访问流量,那么我相信业务愿意被技术驱动,会同意订单的延迟问题,这样便是技术驱动业务。

我相信大部分人还都处于业务驱动技术的方向吧。所以你既然不能驱动业务,那就请拥抱业务变化吧。

代码设计

一直在做 Java 后端的项目,经常会有一些变动,我相信大家也都遇到过。

比如当我们写一段代码的时候,我们考虑将需求映射成代码的状态模式,突然有一天,状态模式里边又添加了很多行为变化的东西,这时候你就挠头了,你硬生生的将状态模式中添加过多行为和变化。

慢慢的你会发现这些状态模式,其实更像是一簇算法,应该使用策略模式,这时你应该已经晕头转向了。

说了这么多,我的意思是,只要你觉得合理,就请将状态模式改为策略模式吧,所有的模式并不是凭空想象出来的,都是基于重构。

Java 编程中没有银弹,请拥抱业务变化,一直思考重构,你就有一个更好的代码设计!

你真的优秀吗?

真不好意思,我取了一个这么无聊的标题。

国外流行一种编程方式,叫做结对编程,我相信国内很多公司都没有这么做,我就不在讲述结对编程带来的好处了,其实就是一边 code review,一边互相提高的一个过程。既然做不到这个,那如何让自己活在自己的世界中不断提高呢?

“平时开发的时候,做出的代码总认为是正确的,而且写法是完美的。”,我相信这是大部分人的心声,还回到刚刚的问题,如何在自己的世界中不断提高呢?

答案就是:

  1. 多看成熟框架的源码
  2. 多回头看自己的代码
  3. 勤于重构
    你真的优秀吗?如果你每周都完成了学习源码,回头看自己代码,然后勤于重构,我认为你就真的很优秀了。

即使也许你只是刚刚入门,但是一直坚持,你就是一个真的会写 Java 代码的程序员了。

技能

UML

不想多讨论 UML 相关的知识,但是我觉得你如果真的会写 Java,请先学会表达自己,UML 就是你说话的语言。

做一名优秀的 Java 程序员,请至少学会这两种 UML 图:

  1. 类图
  2. 时序图

clean code

我认为保持代码的简洁和可读性是代码的最基本保证,如果有一天为了程序的效率而降低了这两点,我认为是可以谅解的,除此之外,没有任何理由可以让你任意挥霍你的代码。

  1. 读者可以看一下 Robert C. Martin 出版的《Clean Code》(代码整洁之道) 这本书
  2. 可以参考美团文章聊聊 clean code
  3. 也可以看一下阿里的 Java 编码规范

无论如何,请保持你的代码的整洁。

Linux 基础命令

这点其实和会写 Java 没有关系,但是 Linux 很多时候确实承载运行 Java 的容器,请学好 Linux 的基础命令。

总结

Java 是一个大体系,今天讨论并未涉及框架和架构相关知识,只是讨论如何写好代码。

本文从写 Java 程序的小方面一直写到大方面,来阐述了如何才能写好 Java 程序,并告诉读者们如何才能提高自身的编码水平。

我希望看到这篇文章的各位都能做一个优秀的 Java 程序员。

概述

有需求本地服务,让公网也能访问,这个时候就需要内网穿透了。

条件

  1. 云服务器
    1. 需公网IP
  2. 域名
    1. 如果需https访问,还需准备证书
  3. nginx
  4. frp服务端
  5. frp客户端

云服务器等

  • 在腾讯、阿里、华为等云服务器商购买服务器
  • 第一次购买最优化,建议直接买5年
  • 云服务器商都有1年的免费的ssl证书
  • 公网访问域名需要备案,按照云服务器商的指引进行操作
  • 域名解析也是在云服务器商配置的

frp

官网
Github

1
2
3
4
5
6
# 编写配置文件frps.ini
# 启动服务端
./frps -c ./frps.ini
# 编写配置文件frpc.ini
# 启动客户端
./frpc -c ./frpc.ini

参考配置

服务端参考配置frps.ini

1
2
3
4
5
6
7
8
9
[common]
# 服务绑定的端口,客户端跟这个一致
bind_port = 7000
# http服务,配置nginx需要
vhost_http_port = 8002
vhost_https_port = 8001
# 验证使用,客户端跟这个一致
token = ***
subdomain_host = ***.com

服务端参考配置nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
server {
listen 80;
server_tokens off;
server_name *****.com;
# 80访问跳转443
return 301 https://*****.com$request_uri;
}


server {
listen 443 ssl;
server_tokens off;
server_name *****.com;
access_log /opt/log/m2.ssl.log;
error_log /opt/log/m2.err.log;

# 静态文件根目录配置
root /opt/site/m2;
index index.html index.htm;

# 证书配置
ssl_certificate conf.d/*****.com.pem;
ssl_certificate_key conf.d/*****.com.key;

ssl_session_timeout 5m;

# ssl算法配置
ssl_protocols SSLv3 TLSv1.2;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_prefer_server_ciphers on;

location / {

# frp服务端转发服务端配置
proxy_pass http://127.0.0.1:8002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

}

服务端参考配置frpc.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[common]
# 跟服务端配置一致
server_addr = *****.com
server_port = 7000
token = *****

[m2]
type = http
# 本地ip
local_ip = 192.168.50.100
# 本地服务端口
local_port = 8080
# 子域名
subdomain = m2

# tcp穿透
[tcp1]
type = tcp
local_ip = 192.168.50.100
local_port = 8088
# 外网可访问的端口
remote_port = 8077
# 域名解析需配置
custom_domains = 77.deep-tek.com

引入依赖

1
implementation 'org.springframework.boot:spring-boot-starter-validation'

说明

  • @Validated
    • 用在类型、方法和方法参数上。但不能用于成员属性(field)
  • @Valid
    • 可以用在方法、构造函数、方法参数和成员属性(field)上

Bean的注解

  • @NotBlank只能用于字符串
  • @NotNull用于其他对象
  • groups分组校验
    • 一个分组校验ok后在校验另外一个分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.Serializable;
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class UserReq implements Serializable {

@NotBlank(message = "姓名不能为空", groups = {IGroupA.class})
private String name;
/**
* 只在分组为IGroupA的情况下进行验证
*/
@NotNull
@Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupA.class})
private Integer age;
@Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
private String phone;
@Email(message = "邮箱格式错误", groups = {IGroupA.class})
private String email;
@NotBlank(message = "地址不能为空", groups = {IGroupB.class})
private String address;
}
  • 嵌套校验需加入注解@Valid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Getter
@Setter
public class DefReq implements Serializable {

@NotBlank(message = "姓名不能为空")
private String name;

@Valid
@NotNull(message = "Bean不能为空")
@Size(min = 1, message = "请选择1个")
private List<TypeBean> beanList;

@Getter
@Setter
public static class TypeBean implements Serializable {

@Min(value = 1, message = "type要大于1")
private Integer type;
}
}

分组

  • 定义分组接口
  • 先校验IGroupA,在校验IGroupB
1
2
3
4
5
6
7
8
9
10
@GroupSequence({Default.class, IGroupA.class, IGroupB.class})
public interface IGroup {

}
public interface IGroupA {

}
public interface IGroupB {

}

Controller

  • @Validated必须要引入分组
  • 无分组引入Default.class
1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.validation.groups.Default;
import org.springframework.validation.annotation.Validated;

@PostMapping("saveUser")
public WebJson saveUser(@Validated(IGroup.class) @RequestBody UserReq userReq) {
log.debug("userReq={}", userReq);
return WebJson.getSuccess("Save User Success");
}
@PostMapping("defVal")
public WebJson defVal(@Validated(Default.class) @RequestBody DefReq req) {
log.debug("req={}", req);
return WebJson.getSuccess("Default Val Success");
}

校验信息返回

  • 将错误信息通过json格式返回给前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestControllerAdvice
public class ExceConf {
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Exception.class)
public WebJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {
WebJson result;
if (e instanceof MethodArgumentNotValidException ee) {
StringBuilder sb = new StringBuilder();
for (FieldError fieldError : ee.getFieldErrors()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(fieldError.getDefaultMessage());
log.debug("field={},{}", fieldError.getField(), fieldError.getDefaultMessage());
}
result = WebJson.getError(sb.toString());
}
return result;
}
}

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
"name": "张三",
"age": 69
}
{
"code": 500,
"message": "地址不能为空",
"result": null
}
{
"name": "张三",
"age": 69,
"address": "test",
"email": "test"
}
{
"code": 500,
"message": "邮箱格式错误",
"result": null
}
// demo/defVal
{
"name":""
}
{
"code": 500,
"message": "姓名不能为空, Bean不能为空",
"result": null
}
{
"name": "name",
"beanList": []
}
{
"code": 500,
"message": "请选择1个",
"result": null
}
{
"name": "name",
"beanList":[{
"type": 0
}]
}
{
"code": 500,
"message": "type要大于1",
"result": null
}
{
"name": "name",
"beanList":[{
"type": 1
}]
}
{
"code": 200,
"message": "Success",
"result": null
}