%date{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN} - [%t] %c{1.} :: %m%n
```
大概是这个样子
```java
2018-08-21 17:12:10.651 [ INFO] - [main] com.xxxx.msearch.toolsframework.main.Main :: 开始读取配置文件
2018-08-21 17:12:11.276 [ INFO] - [main] com.xxxx.msearch.toolsframework.main.Main :: org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
2018-08-21 17:12:11.277 [ INFO] - [main] com.xxxx.msearch.toolsframework.main.Main :: org.quartz.threadPool.threadCount=2
2018-08-21 17:12:11.277 [ INFO] - [main] com.xxxx.msearch.toolsframework.main.Main :: org.quartz.threadPool.threadPriority=5
```
> 日志必须显示 `日志等级`, 时间精确到毫秒
以前也说过, 显示日志等级,可以配合插件来高亮不同日志等级, 还可以设置声音提示.
显示日志等级也有利于在现网搜索特定级别的日志.
以后直接给运维说需要某段时间特定等级或关键字的日志, 要是运维直接扔全部的日志过来, 麻烦把下面的脚本发给他
```shell
# sed -n '/开始时间/,/结束时间/p' all.log |grep '关键字' | > snippet.log
sed -n '/2018-08-21 16:27:57.569/,/2018-08-21 16:36:14.604/p' all.log |grep 'INFO' | > snippet.log
```
## 统一输出路径
具体配置需要查看已修改的模块
log4j.xml
```xml
```
log4j2.xml
```xml
/usr/logs
```
测试环境和生成环境日志配置统一输出到文件, 且文件统一保存在一个日志目录下
以前的方式是将日志保存在 tomcat 的 log 目录下或者通过 JVM 启动的日志保存在启动目录下
这样不好统一管理日志, 每次查看日志时, 还得记住哪个 tomcat 下是哪个应用.
因此考虑将日志全部统一管理在同一个目录下, 通过应用名区分日志.
> 1.应用名配置使用 '-' 分隔, 全部采用小写
不要用大写, linux 下还得按个大小写切换键, 麻烦
```java
async-tool
iavp-proxy
migu-game-service
redis-cache
service-meeting
```
**请按照上面的命名方式命名应用名**
> 2.本地开发时需要修改为自己电脑的一个保存路径
log4j.xml
```xml
```
log4j2.xml
```xml
你的本机路径
```
新框架中不需要这样修改, 能根据不同的环境选择输出路径;
已修改的模块日志配置默认全部输出到 `/usr/logs/` 目录下, 如果是本地开发时, 没有这个目录肯定会报错. 因此需要修改为你本地的一个目录
**如果是 windows, 路径需要转义**
比如我会建立一个专门的日志目录, 将所有的软件日志, 开发日志都输出到这个目录下, 能方便的管理日志
![20241229154732_GNaVfMic.webp](https://cdn.dong4j.site/source/image/20241229154732_GNaVfMic.webp)
推荐大家也采用这种方式
> 3.本地开发时, 需要修改日志等级
现在的日志配置针对 2 种环境
**本地开发环境**
本地开发时, 需要查看 DEBUG 信息, 且只用输出到 console
**现网环境**
现网部署时, 最低日志等级为 INFO 就好, 不需要输出 debug 级别日志, 且只需要输出到文件
log4j.xml
```xml
...
```
log4j2.xml
```xml
```
以上需要修改的配置, 在新框架中都不需要手动修改
### 日志配置修改方式
其他未修改的模块, 只需要参考已修改的模块配置, 直接拷贝后, 需要修改的地方:
1. 应用名
2. 日志输出路径, 统一为 /usr/logs/应用名
3. start.sh
start.sh 中会根据 properties 中的日志配置保存日志文件, 这里统一后, 不使用这种方式, 将日志相关配置全部迁移到 log4j.xml/log4j2.xml 中, 因此需要注释掉 start.sh 对日志的相关配置, 具体可参考已修改的模块
## 日志最佳实践
现有代码存在的最大问题就是排查问题时, 没有日志可看, 不得不加入日志后再部署再看问题, 这样就很尴尬
**合理的日志等级以及日志埋点能够快速定位问题**
针对现在代码中存在的问题和以后的迁移工作, 在做需求开发时, 尽量按照以下方式修改日志相关的代码.
### 删除 printStackTrace()
**删除所有的 printStackTrace() 方法 , 改用日志输出**
e.printStackTrace 会直接输出到 System.err (如果是 tomcat 部署, 就会输出到 catalina.out)
我们的所有日志配置全部通过 log4j.xml 或者 log4j2.xml 控制,
```java
try {
// do something
} catch (Exception e) {
// todo 删除 printStackTrace(), 改用 log.error 输出
e.printStackTrace();
}
```
### 使用 slf4j api
### 正确使用 error 日志级别
```java
public void error(String msg, Throwable t);
```
错误的做法:
```java
# 框架会发现最后一个参数是多余的,并查看其是否是一个异常对象,如果是则输出堆栈,否则忽略
log.error("Failed to format {}", s, e);
```
### 使用 占位符 代替 连接符, 提高效率
```java
log.info("解析错误,错误码:" + status);
```
替换为
```java
log.info("解析错误,错误码: {}", status);
```
## 基本的日志编码规范
> 以下是规范的日志写法, 希望以后开发时, 注意以下几点
1. 获取 log, 日志对象名统一使用 **log**
如果有父类, 统一在父类中获取 log
```java
protected log log = logFactory.getlog(getClass());
```
如果没有父类, 在当前类中获取 log
```java
private static final log log = logFactory.getlog(类名.class);
```
可以使用 live templates 快速输入, 需要自己设置
![20241229154732_orhs0nZm.webp](https://cdn.dong4j.site/source/image/20241229154732_orhs0nZm.webp)
以上方式是没有使用 lombok 插件的情况, 如果使用, 直接在类上加 `@Slf4j` 即可, 然后使用 `log` 对象打印日志
> 如果使用新框架, 可以使用 `Logs` 工具类打印日志
2. 输出 Exceptions 的全部 Throwable 信息,因为 log.error(msg) 和 log.error(msg,e.getMessage()) 这样的日志输出方法会丢失掉最重要的 StackTrace 信息。
```java
void foo(){
try{
// do something
} catch (Exception e) {
log.error(e.getMessage()); // 错误
log.error("Bad things", e.getMessage()); // 错误
log.error("Bad things", e); // 正确
// error 允许拼接字符串, 因为 error 毕竟没有 info 和 debug 多, 而且需要输出参数信息时, 也只有这种方式
log.error("Bad things " + user, e);
}
}
```
3. **不允许**记录日志后又抛出异常,因为这样会多次记录日志,只允许记录一次日志.
```java
void foo() throw LogException {
try{
// do something
} catch (NoUserException e) {
log.error("No user available", e);
// 这里抛出异常后, 上级又会处理一次异常
throw new UserServiceException("Nouseravailable", e);
}
}
```
4. **不允许**出现 System print(包括 System.out.println 和 System.error.println) 语句
5. **不允许**出现 printStackTrace
```java
void foo() throw LogException {
try{
// do something
} catch (NoUserException e) {
e.printStackTrace(); // 错误
log.error("No user available", e);
}
}
```
6. 使用 slf4j 代替 log4j
**slf4j 中的占位符—不再需要冗长的级别判断**
在 log4j 中,为了提高运行效率,往往在输出信息之前,还要进行级别判断,以避免无效的字符串连接操作。如下:
```java
if (log.isDebugEnabled()){
log.debug("debug:" + name);
}
```
slf4j 巧妙的解决了这个问题:先传入带有占位符的字符串,同时把其他参数传入,在 slf4j 的内容部实现中,如果级别合适再去用传入的参数去替换字符串中的占位符,否则不用执行。
```java
log.info("{} is {}", new String[]{“x",“y"});
```
7. 不在循环中打印日志
```java
void read() {
while (hasNext()) {
try {
readData();
} catch {Exception e) {
// this isn’t recommend
log.error("error reading data", e);
}
}
}
```
如果 readData() 抛出异常并且 hasNext() 返回 true,这段代码就会不停在打印日志
```java
void read() {
int exceptionsThrown = 0;
while (hasNext()) {
try {
readData();
} catch {Exception e) {
if (exceptionsThrown < THRESHOLD) {
log.error(“error reading data", e);
exceptionsThrown++;
} else {
// Now the error won’t choke the system.
}
}
}
}
```
还有一个方法就是把日志操作从循环中去掉,在另外的地方进行打印,只记录第一个或者最后一个异常就好了
## [避免调试陷阱:依赖日志的编程习惯](https://blog.dong4j.site/posts/286572f8.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
开发中日志这个问题,每个公司都强调,也制定了一大堆规范,但根据实际情况看,效果不是很明显,主要是这个东西不好测试和考核,没有日志功能一样跑啊。
但是没有日志, 一旦系统出现问题, 将导致排查问题时困难重重.
因此好的日志输出有利于快速定位问题
但是我们在什么时候打印日志? 需要打印什么信息? 用什么日志级别?
这些问题都将应用我们排查问题时的速度
因此这里制定一个日志规范, 将日志相关的常识性问题做一个总结.
## 日志等级说明
1. trace, debug: 理论上 "不属于错误", 只是打印一些状态, 提示信息, 以便 `开发过程` 中观察.
2. info: 理论上 "不属于错误", 只是一些提示性的信息, 但是即使在开发完成, 正式上线的系统中, 也有保留的价值.
3. warn: 属于轻微的 "警告", 程序中出现了一些异常情况, 但是影响不大, 还可以正常使用.
4. error: 属于 "普通的错误", 在程序可以控制的范围内, 不会造成连锁影响或巨大影响.
5. fatal: 属于 "致命错误", 可导致整个系统或者一系列功能无法使用, 甚至导致系统瘫痪, 关闭.
## 规范
> 1. 日志必须显示 `日志等级`, 时间精确到毫秒
以前也说过, 显示日志等级, 可以配合插件来高亮不同日志等级, 还可以设置声音提示.
显示日志等级也有利于在现网搜索特定级别的日志.
以下是利用 `Grep Console` 插件显示的效果, 能区分不同的日志等级
用以下命令获取某段时间特定等级或关键字的日志
```bash
# sed -n '/开始时间/,/结束时间/p' all.log | grep '关键字' | > snippet.log
sed -n '/2018-08-21 16:27:57.569/,/2018-08-21 16:36:14.604/p' all.log | grep 'INFO' | > snippet.log
```
> 2. 修改(包括新增)操作必须打印日志
大部分问题都是修改导致的。数据修改必须有据可查
> 3. 条件分支必须打印条件值,重要参数必须打印
尤其是分支条件的参数,打印后就不用分析和猜测走那个分支了,很重要!如下面代码里面的 messageType,一定要打印值,因为他决定了代码走那个分支
```java
/**
* Instance rocket mq handler.
* 根据消息类型选择处理器处理消息
*
* @param messageType the message type
* @return the rocket mq handler
*/
@Override
public MessageHandler instance(String messageType) {
Class extends MessageHandler> cls = null;
log.info("message type = {}", messageType);
switch (MessageType.valueOf(messageType)) {
case BUSINESS_LOG:
cls = BusinessLogHandler.class;
break;
case WORD_ANALYSIS:
cls = WordAnalysisHandler.class;
break;
default:
log.error("Unknown MessageType, messageType = {} ", messageType);
break;
}
return SpringContext.getInstance(cls);
}
```
> 4. 数据量大的时候需要打印数据量
前后打印日志和最后的数据量,主要用于分析性能,能从日志中知道查询了多少数据用了多久.
自己视情况而决定是否打印,我一般建议打印.
> 5. 不要使用 System.out.println() 来记录日志
使用 System.out.println() 不会输出到日志文件.
本地开发时, 觉得方便就直接使用 System.out.println()输出, 本地是看得到信息, 但是一到线上环境, 日志全部输出到文件中, 使用 System.out.println()
输出的信息就全部没有了.
System.out.println() 一般是开发时输出不重要的信息, 建议使用 log.debug 代替.
使用 lombok, 应该比 System.out.println() 更方便吧!
## 建议
日志这个东西,更多是靠自觉,项目组这么多人,不可能一个一个看代码,然后加日志.
打印日志更多的是一种习惯, 需要有意识的去培养这种习惯.
1. 不要依赖 debug,多依赖日志。
别人面对对象编程,你面对 debug 编程. 有些人无论什么语言,最后都变成了面对 debug 编程....
这个习惯非常非常不好!debug 会让你写代码的时候偷懒不打日志,而且很浪费时间. 改掉这个恶习.
只有在必要的情况下才会 debug, 更多的是通过日志来分析流程, 因为在生产环境, 尤其是现公司的生产环境, 远程 debug 是不可能的, 只能依赖日志.
代码开发测试完成之后不要急着提交,先跑一遍看看日志是否看得懂.
日志是给人看的,只要热爱编程的人才能成为合格程序员, 不要匆匆忙忙写完功能测试 ok 就提交代码,日志也是功能的一部分. 要有精益求精的工匠精神!
## 日志最佳实践
现有代码存在的最大问题就是排查问题时, 没有日志可看, 不得不加入日志后再部署再看问题, 这样就很尴尬
**合理的日志等级以及日志埋点能够快速定位问题**
针对现在代码中存在的问题和以后的迁移工作, 在做需求开发时, 尽量按照以下方式修改日志相关的代码.
### 删除 printStackTrace()
**删除所有的 printStackTrace() 方法 , 改用日志输出**
e.printStackTrace 会直接输出到 System.err (如果是 tomcat 部署, 就会输出到 catalina.out)
我们的所有日志配置全部通过 log4j.xml 或者 log4j2.xml 控制,
```java
try {
// do something
} catch (Exception e) {
// todo 删除 printStackTrace(), 改用 log.error 输出
e.printStackTrace();
}
```
### 使用 slf4j api
### 正确使用 error 日志级别
```java
public void error(String msg, Throwable t);
```
错误的做法:
```java
# 框架会发现最后一个参数是多余的,并查看其是否是一个异常对象,如果是则输出堆栈,否则忽略
log.error("Failed to format {}", s, e);
```
### 使用 占位符 代替 连接符, 提高效率
```java
log.info("解析错误,错误码:" + status);
```
替换为
```java
log.info("解析错误,错误码: {}", status);
```
## 基本的日志编码规范
> 以下是规范的日志写法, 希望以后开发时, 注意以下几点
1. 获取 log, 日志对象名统一使用 **log**
如果有父类, 统一在父类中获取 log
```java
protected log log = logFactory.getlog(getClass());
```
如果没有父类, 在当前类中获取 log
```java
private static final log log = logFactory.getlog(类名.class);
```
可以使用 live templates 快速输入, 需要自己设置
以上方式是没有使用 lombok 插件的情况, 如果使用, 直接在类上加 `@Slf4j` 即可, 然后使用 `log` 对象打印日志
> 如果使用新框架, 可以使用 `Logs` 工具类打印日志
2. 输出 Exceptions 的全部 Throwable 信息,因为 log.error(msg) 和 log.error(msg,e.getMessage()) 这样的日志输出方法会丢失掉最重要的 StackTrace
信息。
```java
void foo(){
try{
// do something
} catch (Exception e) {
log.error(e.getMessage()); // 错误
log.error("Bad things", e.getMessage()); // 错误
log.error("Bad things", e); // 正确
// error 允许拼接字符串, 因为 error 毕竟没有 info 和 debug 多, 而且需要输出参数信息时, 也只有这种方式
log.error("Bad things " + user, e);
}
}
```
3. **不允许** 记录日志后又抛出异常,因为这样会多次记录日志,只允许记录一次日志.
```java
void foo() throw LogException {
try{
// do something
} catch (NoUserException e) {
log.error("No user available", e);
// 这里抛出异常后, 上级又会处理一次异常
throw new UserServiceException("Nouseravailable", e);
}
}
```
4. **不允许** 出现 System print(包括 System.out.println 和 System.error.println) 语句
5. **不允许** 出现 printStackTrace
```java
void foo() throw LogException {
try{
// do something
} catch (NoUserException e) {
e.printStackTrace(); // 错误
log.error("No user available", e);
}
}
```
6. 使用 slf4j 代替 log4j
**slf4j 中的占位符—不再需要冗长的级别判断**
在 log4j 中,为了提高运行效率,往往在输出信息之前,还要进行级别判断,以避免无效的字符串连接操作。如下:
```java
if (log.isDebugEnabled()){
log.debug("debug:" + name);
}
```
slf4j 巧妙的解决了这个问题:先传入带有占位符的字符串,同时把其他参数传入,在 slf4j 的内容部实现中,如果级别合适再去用传入的参数去替换字符串中的占位符,否则不用执行。
```java
log.info("{} is {}", new String[]{“x",“y"});
```
7. 不在循环中打印日志
```java
void read() {
while (hasNext()) {
try {
readData();
} catch {Exception e) {
// this isn’t recommend
log.error("error reading data", e);
}
}
}
```
如果 readData()抛出异常并且 hasNext() 返回 true,这段代码就会不停在打印日志
```java
void read() {
int exceptionsThrown = 0;
while (hasNext()) {
try {
readData();
} catch {Exception e) {
if (exceptionsThrown < THRESHOLD) {
log.error(“error reading data", e);
exceptionsThrown++;
} else {
// Now the error won’t choke the system.
}
}
}
}
```
还有一个方法就是把日志操作从循环中去掉,在另外的地方进行打印,只记录第一个或者最后一个异常就好了
## 日志追踪系统
仅仅依赖以上日志规范只能做到单个应用的日志规范记录, 但是这些分散的数据对于问题排查,或是流程优化都
帮助有限.
对于一个跨进程 / 跨线程的场景, 汇总收集并分析海量日志就显得尤为重要.
要能做到追踪每个请求的完整调用链路, 收集调用链路上每个服务的性能数据, 计算性能数据和比对性能指标等能有效排查问题, 分析系统瓶颈.
最后还能对日志进行大数据分析, 带来更高的利润.
因此以后会考虑做日志追溯系统
这里有一篇以前写的 [日志追踪系统设计](/nodes/log-trace-design.md) 和 [日志追踪系统实现](/nodes/log-trace-implement.md)
## [SSO单点登录优化:Shiro认证过程详解与代码封装技巧](https://blog.dong4j.site/posts/205ef910.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
多个模块有登录需求, 但是代码都是相互拷贝, 没有做统一处理
## 优化方案
将登录逻辑封装成模块, 作为插件提供服务
## shiro 认证过程
### 1. 收集实体/凭据信息
```java
UsernamePasswordToken token = new UsernamePasswordToken(username, password, true);
```
UsernamePasswordToken 支持最常见的用户名/密码的认证机制。同时,由于它实现了 Rarraydsj@163.comemberMeAuthenticationToken 接口,我们可以通过令牌设置“记住我”的功能。
但是,“已记住”和“已认证”是有区别的:
已记住的用户仅仅是非匿名用户,你可以通过 subject.getPrincipals() 获取用户信息。但是它并非是认证通过的用户,当你访问需要认证用户的功能时,你仍然需要重新提交认证信息。
这一区别可以参考淘宝网站,网站会默认记住登录的用户,再次访问网站时,对于非敏感的页面功能,页面上会显示记住的用户信息,但是当你访问网站账户信息时仍然需要再次进行登录认证
### 2. 提交实体/凭据消息
```java
SecurityUtils.getSubject().login(token);
```
### 3. 认证
如果自定义 Realm 实现, 但执行第二步中的 login() 时, 会回调 `doGetAuthenticationInfo()`
```java
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
// 获取基于用户名和密码的令牌
// 实际上这个 token 是从 LoginController 里面 SecurityUtils.getSubject().login(token) 传过来的
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String username = token.getUsername();
if (!StringUtils.isEmpty(username)) {
// 从数据库中查询用户用信息
LoginModel model = authorityService.getLoginModel(username);
// 原来没有判断 model 是否为 null 的语句, 会造成 model.getPassword() 语句 空指针异常
if(model != null){
// 此处无需比对,比对的逻辑 Shiro 会做, 我们只需返回一个和令牌相关的正确的验证信息
return new SimpleAuthenticationInfo(StringUtils.upperCase(username), model.getPassword(), getName());
}
}
return null;
}
```
### 4. 认证处理
```java
try {
SecurityUtils.getSubject().login(token);
} catch (AuthenticationException e) {
model.addAttribute("message", "用户名或密码错误或用户已被禁用");
return "login";
}
```
发生异常时, 给出提示信息, 返回到登录页面
如果 login 方法执行完毕且没有抛出任何异常信息,那么便认为用户认证通过。之后在应用程序任意地方调用 SecurityUtils.getSubject() 都可以获取到当前认证通过的用户实例,使用 subject.isAuthenticated() 判断用户是否已验证都将返回 true.
相反,如果 login 方法执行过程中抛出异常,那么将认为认证失败
### 4. 登出操作
登出操作可以通过调用 subject.logout() 来删除你的登录信息,如:
```
SecurityUtils.getSubject().logout();
```
### SimpleAuthenticationInfo
SimpleAuthenticationInfo 这里原理很简单,又有一些值得挖掘的东西。
```java
//此处使用的是user对象,不是username
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user,
user.getPassword(),
getName()
);
```
这个东西是在 realm 中的,第一个参数 user,这里好多地方传的时候都是 user 对象,但是都在备注用户名。可是我如果传入 username,就会报类型转换问题。
但是在开涛大神的博客中,无状态的 shiro 里,那边给出的例子是传 username。我自己测试的,可以传 username,也可以传 user 对象,仅限他那边一段代码。网上有文章说,这里其实是 user 和 username 的集合,后端是分两个字段接收的。由于时间的问题,没有深入里了解这块,传 user 对象是 OK 的。
第二个字段是 user.getPassword(),注意这里是指从数据库中获取的 password。
第三个字段是 realm,即当前 realm 的名称。
看了几篇文章介绍说,这块对比逻辑是先对比 username,但是 username 肯定是相等的,所以真正对比的是 password。从这里传入的 password(这里是从数据库获取的)和 token(filter 中登录时生成的)中的 password 做对比,如果相同就允许登录,不相同就抛出异常。
如果验证成功,最终这里返回的信息 authenticationInfo 的值与传入的第一个字段的值相同(我这里传的是 user 对象)。
## [从零开始:lanproxy与nginx配置攻略](https://blog.dong4j.site/posts/81cae783.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
需要的环境:
1. 具有公网 IP 的服务器, 运行 proxy-server
2. 一台内网 pc 或服务器, 运行 proxy-client
3. lanproxy
4. nginx
5. JDK
## 准备工作
使用 阿里云 服务器, ip 为 `111.111.111.111`, 已经安装好了 nginx 和 JDK8
下载 proxy-server 和 proxy-client
https://github.com/ffay/lanproxy/releases
修改 hosts 文件
```
111.111.111.111 ivr.wechat.com
111.111.111.111 local.ivr.wechat.com
```
## proxy-server 搭建
1. 解压 proxy-server.zip
2. 修改配置文件 `conf/config.properties`, 修改用户名和密码
3. 启动服务 `/usr/local/proxy-server/bin/startup.sh`
config.properties
```lua
server.bind=0.0.0.0
# 普通端口
server.port=4900
server.ssl.enable=true
server.ssl.bind=0.0.0.0
# ssl 端口
server.ssl.port=4993
server.ssl.jksPath=test.jks
server.ssl.keyStorePassword=123456
server.ssl.keyManagerPassword=123456
server.ssl.needsClientAuth=false
config.server.bind=0.0.0.0
# server 后台端口
config.server.port=8090
# 用户名
config.admin.username=admin
# 密码
config.admin.password=admin
```
4. 使用 nginx 反向代理
访问 `http://ivr.wechat.com/` 代理到 `http://127.0.0.1:8090`
```lua
server {
listen 80;
server_name ivr.wechat.com;
charset utf-8;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 1024m;
client_body_buffer_size 128k;
client_body_temp_path data/client_body_temp;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
proxy_temp_path data/proxy_temp;
proxy_pass http://127.0.0.1:8090;
}
}
```
访问 http://ivr.wechat.com/
![20241229154732_eWVWuj5D.webp](https://cdn.dong4j.site/source/image/20241229154732_eWVWuj5D.webp)
## proxy-server 配置
1. 登录 proxy-server,添加客户端,输入客户端备注名称,生成随机密钥,提交添加
![20241229154732_fr4EBIOa.webp](https://cdn.dong4j.site/source/image/20241229154732_fr4EBIOa.webp)
2. 客户端列表中,配置管理中,都会出现新添加的客户端
![20241229154732_TQkmxEDb.webp](https://cdn.dong4j.site/source/image/20241229154732_TQkmxEDb.webp)
3. 单击配置管理中的客户端,添加配置(每个客户端可以添加多个配置)
![20241229154732_FnzKC41D.webp](https://cdn.dong4j.site/source/image/20241229154732_FnzKC41D.webp)
- 代理名称,推荐输入客户端要代理出去的端口,或者是客户端想要发布到公网的项目名称
- 公网端口,填入一个服务器空闲端口,用来转发请求给客户端
- 代理 IP 端口,填入客户端端口,公网会转发请求给该客户端端口
## proxy-client 搭建
1. 解压 proxy-client
2. 修改配置文件
```lua
# 与在proxy-server配置后台创建客户端时填写的秘钥保持一致
client.key=0b1bad2253f945a28a2ea7d3e8f35545
ssl.enable=true
ssl.jksPath=test.jks
ssl.keyStorePassword=123456
# 这里填写实际的proxy-server地址;没有服务器默认即可,自己有服务器的更换为自己的proxy-server(IP)地址
server.host=ivr.wechat.com
#default ssl port is 4993
#proxy-server ssl默认端口4993,默认普通端口4900
#ssl.enable=true时这里填写ssl端口,ssl.enable=false时这里填写普通端口
server.port=4993
```
1. 执行 `bin/startup.sh` 启动 client
启动成功后, 会显示 `在线` 标识
![20241229154732_reJKuvzx.webp](https://cdn.dong4j.site/source/image/20241229154732_reJKuvzx.webp)
最后直接访问 `http://ivr.wechat.com:50002` 将会转发请求到 127.0.0.1:8080
## 优化
nginx 再添加一次转发
```lua
server {
listen 80;
server_name local.ivr.wechat.com;
charset utf-8;
location /{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 1024m;
client_body_buffer_size 128k;
client_body_temp_path data/client_body_temp;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
proxy_temp_path data/proxy_temp;
proxy_pass http://127.0.0.1:50002;
}
}
```
直接访问 `http://local.ivr.wechat.com` 将会转发到 proxy-server 的 50002 端口, 相当于直接访问 `http://ivr.wechat.com:50002`,
最后会转发到内网的 `127.0.0.1:8080`
## [动态调整日志:JMX与Zookeeper的强大组合](https://blog.dong4j.site/posts/8872e19a.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
## why
为了减少日志文件的数量, 生产环境的日志等级都是 Error, 但是当遇到问题时, 错误日志可能不能快速准确的定位出错的地方, 如果能在不重启应用的情况下,
修改日志级别并且生效, 能更快的发现出错的地方.
## what
这里选择使用 JMX 来实现日志级别动态修改.
JMX (Java Management Extensions) 是管理 Java 的一种扩展. 这种机制可以方便的管理, 监控正在运行中的 Java 程序. 常用于管理线程, 内存, 日志
Level, 服务重启, 系统环境等.
实现一个被 JMX 托管的 MBean 的方式:
1. MBean 的接口必须以 MBean 结尾, 比如 XxxxMBean
2. 实现必须以 Xxxx 命名因为接口定义是 XxxxMBean
logback 定义的 MBean
![20241229154732_UPs9YUUI.webp](https://cdn.dong4j.site/source/image/20241229154732_UPs9YUUI.webp)
jconsole 查看 MBean
![20241229154732_kefuJoBn.webp](https://cdn.dong4j.site/source/image/20241229154732_kefuJoBn.webp)
## how
动态修改日志级别的思路:
![20241229154732_WaZ9o14Z.webp](https://cdn.dong4j.site/source/image/20241229154732_WaZ9o14Z.webp)
1. API 调用 DynamicChangeLogLevel 修改日志级别
2. DynamicChangeLogLevel 通过 LogBackMBean 修改日志级别
3. 使用责任链, 修改 xxx-api 后, 后面相关的服务都会被修改
### 存在的问题
**1. 在集群环境, 因为有负债均衡, 不同的请求被负载到不同的机器上面, 前面修改了日志级别, 下一次有可能不会生效**
![20241229154732_zSuSD7Y6.webp](https://cdn.dong4j.site/source/image/20241229154732_zSuSD7Y6.webp)
**2. 不能单独修改具体应用的日志级别**
### 解决方案
使用 Zookeeper Watcher.
1. 每个应用看做一个单独的节点, 启动的时候向 Zookeeper 注册以项目服务名命名的节点, 并把 logback.xml 中设置的日志级别写入节点, 最后对这个节点监听.
2. API 调用时, 传入 applicationName 和 level, 修改具体节点下的数据
3. 节点数据被修改, 触发 watcher, 调用 LogBackMBean 修改日志级别
具体流程如下:
![20241229154732_pbG4MaHS.webp](https://cdn.dong4j.site/source/image/20241229154732_pbG4MaHS.webp)
### 具体步骤
1. 修改 logback.xml 配置, 添加 `` 开启 JMX, 添加 com.xxx 的日志级别
```xml
...
```
设置一个 `` 的原因在于, 当需要查找执行流程时, 只需要将 com.xxx 设置为 INFO,
这样只会输出 `com.xxx` 包及子包中的 INFO 信息.
如果没有 `com.xxx`, 我们只有设置 ROOT 为 INFO, 这样会输出诸如 dubbo, zookeeper 等第三方包中的所有 INFO 信息.
2. 添加一个 `ServerListener`, 一是解决 [动态修改日志级别时内存溢出](https://logback.qos.ch/manual/jmxConfig.html), 二是应用启动完成后向
Zookeeper 创建节点
```java
public class ServerListener implements ServletContextListener {
private final Logger log = LoggerFactory.getLogger(this.getClass());
public void contextDestroyed(ServletContextEvent contextEvent) {
// 防止动态修改日志级别时内存溢出
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.stop();
}
public void contextInitialized(ServletContextEvent contextEvent) {
String applicationName = contextEvent.getServletContext().getServletContextName();
log.info("=================================");
log.info("system [{}] start finish!!!", applicationName);
log.info("=================================");
log.info("servlet path [{}]", System.getProperty(contextEvent.getServletContext().getServletContextName()));
log.info("create zookeeper node start");
String host = "127.0.0.1:2181";
String defaultLogLevel = DynamicChangeLogLevel.getCurrentlyLevel(new LogNode());
new LogNodeOperation(host, applicationName, defaultLogLevel);
}
}
```
3. 实现 ChangeLogLevel API
### 部署问题
因为 MBean 是通过 ObjectName 来获取对象, logback 的默认 OBjectName
为 `ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator`
当在同一个 Tomcat 中部署多个应用时, 每个 Web 应用程序中的记录器上下文相关联的各种实例将会相互冲突
**解决方法**
在 logback.xml 设置 contextName
```xml
${project.artifactId}
...
```
![20241229154732_vmTKfeym.webp](https://cdn.dong4j.site/source/image/20241229154732_vmTKfeym.webp)
这样就能区分不同的应用
![20241229154732_30Clgp3H.webp](https://cdn.dong4j.site/source/image/20241229154732_30Clgp3H.webp)
# 新需求
要明确不同服务器上的不同应用, 能具体修改某一台服务器上的某一个应用的日志级别
## [Spring AOP 动态代理深度解析:LTW 技术揭秘](https://blog.dong4j.site/posts/95aea2e1.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
代码织入实现方式:
1. 静态代理
1. AspectJ 织入器 weaver)
1. compile-time weaving 使用 aspectj 编译器进行编译源码
2. post-compile weaving 对 class 文件进行织入
3. load-time weaving(LTW) 当 class loader 加载类的时候,进行织入
2. 动态代理
1. JDK 动态代理 (接口)
2. CGlib(类)
这里使用 [AspectJ LTW](http://www.eclipse.org/aspectj/doc/next/devguide/ltw-configuration.html) 实现, 这种方式在类加载器织入代码.
- **编译器织入** 会造成编译速度变慢, 而且必须使用 ajc 编译器
- **动态代理** 会生成大量代理类, 加速内存消耗
因此使用 **类加载期织入** 相对于其他两种方式, 更加轻便.
LTW(Load Time Weaver),即加载期切面织入,是 ApsectJ 切面织入的一种方式,它通过 JVM 代理在类加载期替换字节码到达织入切面的目的。
## 具体实现
首先定义个切面类,该切面功能非常简单,就是在 `com.xxx.server.rest.resource.impl` 及其所有子包下所有的类的 public 方法调用后打印执行时间
```java
@Slf4j
@Aspect
public class ProfilingAspect {
@Pointcut("execution(* com.xxx.server.rest.resource.impl..*.*(..))")
public void api() {
}
@Around("api()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
sw.start(pjp.getSignature().getName());
return pjp.proceed();
} finally {
sw.stop();
log.debug("AOP --> {} ms", sw.getTotalTimeMillis());
}
}
}
```
使用了 AspectJ 注解,想要了解更多的 Aspect 注解使用可以查看 AspectJ 相关的文档,在 `https://github.com/eclipse/org.aspectj/tree/master/docs`
下面有个 quick5.doc 文档列出了所有的注解,也可以查看源代码或者反编译 aspectweaver.jar,查看里面有哪些注解,在 org.aspectj.lang.annotation 这个包下面
编写目标测试 bean:
```java
public class LTWBean {
public void run() {
System.out.println("LTWBean run...");
}
}
```
编写一个 XML 文件定义切面,该文件名及其所在路径只能固定的几种:META-INF/aop.xml、META-INF/aop-ajc.xml、org/aspectj/aop.xml,文件内容如下:
```xml
>
```
**存在的坑**
按照官方文档配置 aop.xml, 一直不生效, 是因为没有将切面类 include
文件定义了切面以及切面编织器,这里的切面会被织入 `com.xxx.server.rest.resource.impl` 及其子包下的所有类。
编织器注入 Spring 容器中,并且定义目标测试 bean:
```xml
```
上面的编织器 bean 的名称必须是 loadTimeWeaver,此外还有一种更简单的配置方式,使用 context 名称空间下的标签:
```xml
```
上面两段配置起到的效果完全是一样的,bean 解析器在解析到 `context:load-time-weaver` 标签时,
会自动生成一个名称为 `loadTimeWeaver` 类型 `org.springframework.context.weaving.DefaultContextLoadTimeWeaver` 的 bean
以及一个类型 `org.springframework.context.weaving.AspectJWeavingEnabler` 的匿名 bean,这段代码在 `LoadTimeWeaverBeanDefinitionParser` 类中.
aspectj-weaving 属性有三个枚举值:
- on
- off
- autodetect
分别是打开、关闭、自动检测
这里设置成 autodetect,容器会检查程序是否定义了切面定义文件(即上面提到的 aop.xml 文件)
代码在 `LoadTimeWeaverBeanDefinitionParser` 的 `isAspectJWeavingEnabled` 方法。
编写测试代码:
```java
BeanFactory context = new ClassPathXmlApplicationContext("spring/beans/ltw/ltw.xml");
LTWBean ltwBean = (LTWBean) context.getBean("ltwBean");
ltwBean.run();
```
运行测试代码,在运行时需要启动一个 JVM 代理,首先需要下载 spring-instrument-xxx.jar 包,在虚拟机参数中添加
```bash
-javaagent:/path/to/spring-instrument-4.3.10.RELEASE.jar
-javaagent:/path/to/aspectjweaver-1.8.10.jar
```
执行测试代码,打印下面日志,说明切面织入成功:
```bash
LTWBean run...
AOP --> xx ms
```
# 字节码转换和虚拟机代理
要了解 LTW 的原理,首先要对 JDK 的字节码转换框架和 JVM 代理有一定的了解:
从 JAVA5 开始,在 JDK 中添加了一个新包 java.lang.instrument,这个包就是字节码转换框架的基础。字节码转换框架是一项非常重要的技术,许多程序监控和调试工具比如
BTrace 都是在这个框架的基础上实现的。这个包下面有两个关键接口:
- ClassFileTransformer:类字节码转换器,提供了一个 transform 方法,这个方法的用来转换提供的类字节码,并返回一个新的替换字节码。不同于 CGLIB、JDK
动态代理等字节码操作技术,ClassFileTransformer 是彻底的替换掉原类,而 CGLIB 和 JDK 动态代理是生成一个新子类或接口实现。
- Instrumentation:这个类提供检测 Java
编程语言代码所需的服务。检测是向方法中添加字节码,以搜集各种工具所使用的数据。由于更改完全是进行添加,所以这些工具不修改应用程序的状态或行为。这种无害工具的例子包括镜像代理、分析器、覆盖分析器和事件记录器。可以通过它的
addTransformer 方法把实现好的字节码转换器(ClassFileTransformer)注册到 JVM 中。
在普通代码中无需实现 Instrumentation 并且创建 Instrumentation 实例,要获取 Instrumentation 实例,可以通过 JVM 代理,JVM 代理可以通过两种方式启动:
## 程序启动时启动代理
在程序启动时指定代理,这时候虚拟机会创建一个 Instrumentation 实例实例传递给代理类的 premain 方法。需要在 META-INF/MANIFEST.MF 中通过
Premain-Class 指定代理入口类,并且代理入口类中必须定义 premain 方法,像上面提到的在运行程序时在虚拟机参数添加 -javaagent 就是在程序启动时指定了代理,我们可以看看
spring-instrument-3.2.9.RELEASE.jar 包中 MANIFEST.MF 文件的内容:
```bash
Manifest-Version: 1.0
Created-By: 1.7.0_55 (Oracle Corporation)
Implementation-Title: spring-instrument
Implementation-Version: 3.2.9.RELEASE
Premain-Class: org.springframework.instrument.InstrumentationSavingAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: false
```
看以看到通过 Premain-Class 指定了 org.springframework.instrument.InstrumentationSavingAge 作为代理入口类,看看 InstrumentationSavingAge
这个类的代码,有一个 premain 方法并且有一个 Instrumentation 参数:
```java
public class InstrumentationSavingAgent {
private static volatile Instrumentation instrumentation;
/**
* Save the {@link Instrumentation} interface exposed by the JVM.
*/
public static void premain(String agentArgs, Instrumentation inst) {
instrumentation = inst;
}
/**
* Return the {@link Instrumentation} interface exposed by the JVM.
* Note that this agent class will typically not be available in the classpath
* unless the agent is actually specified on JVM startup. If you intend to do
* conditional checking with respect to agent availability, consider using
* {@link org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver#getInstrumentation()}
* instead - which will work without the agent class in the classpath as well.
* @return the {@code Instrumentation} instance previously saved when
* the {@link #premain} method was called by the JVM; will be {@code null}
* if this class was not used as Java agent when this JVM was started.
* @see org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver#getInstrumentation()
*/
public static Instrumentation getInstrumentation() {
return instrumentation;
}
}
```
# 程序运行时启动代理
还有一种方式是在程序启动之后启动代理,这种情况下可以在不暂停应用程序的情况下动态向注册类转换器对已加载的类进行重定义。这种方式需要在
META-INF/MANIFEST.MF 文件中通过 Agent-Class 指定代理入口类,并且入口代理类中定义 agentmain 方法,虚拟机把 Instrumentation 实例传递给这个
agentmain 方法,下面代码是 Druid 框架(Druid 是阿里巴巴一个开源的连接池框架)中拷贝过来的一段代码,演示了如何在应用运行时启动一个 JMX 代理:
```java
private static String loadManagementAgentAndGetAddress(int vmid) throws IOException {
VirtualMachine vm = null;
String name = String.valueOf(vmid);
try {
vm = VirtualMachine.attach(name);
} catch (AttachNotSupportedException x) {
throw new IOException(x.getMessage(), x);
}
String home = vm.getSystemProperties().getProperty("java.home");
// Normally in ${java.home}/jre/lib/management-agent.jar but might
// be in ${java.home}/lib in build environments.
String agent = home + File.separator + "jre" + File.separator + "lib" + File.separator + "management-agent.jar";
File f = new File(agent);
if (!f.exists()) {
agent = home + File.separator + "lib" + File.separator + "management-agent.jar";
f = new File(agent);
if (!f.exists()) {
throw new IOException("Management agent not found");
}
}
agent = f.getCanonicalPath();
try {
vm.loadAgent(agent, "com.sun.management.jmxremote");
} catch (AgentLoadException x) {
throw new IOException(x.getMessage(), x);
} catch (AgentInitializationException x) {
throw new IOException(x.getMessage(), x);
}
// get the connector address
Properties agentProps = vm.getAgentProperties();
String address = (String) agentProps.get(LOCAL_CONNECTOR_ADDRESS_PROP);
vm.detach();
return address;
}
```
运行 management-agent.jar 作为代理,来看看 management-agent.jar 中的 META-INF/MANIFEST.MF 文件,同时指定了 Premain-Class 和
Agent-Class,代理启动之后虚拟机会调用代理入口类的 agentmain 方法,需要注意的是 Agent 类只有一个 String 参数的 agentmain 并没有定义带
Instrumentation 参数的 agentmain,因为 JMX 代理并不需要 Instrumentation 实例:
```bash
Manifest-Version: 1.0
Premain-Class: sun.management.Agent
Created-By: 1.5.0 (Sun Microsystems Inc.)
Agent-Class: sun.management.Agent
```
# 实现原理
了解了 JDK 字节码框架和虚拟机代理之后,分析 LTW 的实现原理就简单得多了。
当容器检查到定义了名称为 loadTimeWeaver 的 bean 时,会注册一个 LoadTimeWeaverAwareProcessor 到容器中,代码在 AbstractApplicationContext 的
prepareBeanFactory 方法中:
```java
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
...
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
...
}
```
LoadTimeWeaverAwareProcessor 是一个 BPP(BeanPostProcessor),这个 BPP 用来处理 LoadTimeWeaverAware 接口的,把 LTW 实例设置到实现了
LoadTimeWeaverAware 接口的 bean 中,从 LoadTimeWeaverAwareProcessor 的 postProcessBeforeInitialization 方法可以看出来:
```java
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof LoadTimeWeaverAware) {
LoadTimeWeaver ltw = this.loadTimeWeaver;
if (ltw == null) {
Assert.state(this.beanFactory != null,
"BeanFactory required if no LoadTimeWeaver explicitly specified");
ltw = this.beanFactory.getBean(
ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME, LoadTimeWeaver.class);
}
((LoadTimeWeaverAware) bean).setLoadTimeWeaver(ltw);
}
return bean;
}
```
再来看下 AspectJWeavingEnabler 这个类的代码,它是一个 BFPP,同时也实现了 LoadTimeWeaverAware 接口,通过上面的分析,loadTimeWeaver 这个 bean
会自动注入到 AspectJWeavingEnabler 类型 bean 中。AspectJWeavingEnabler 的 postProcessBeanFactory 方法直接调用 enableAspectJWeaving
方法,来看看这个方法的代码:
```java
public static void enableAspectJWeaving(LoadTimeWeaver weaverToUse, ClassLoader beanClassLoader) {
if (weaverToUse == null) {
if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {
weaverToUse = new InstrumentationLoadTimeWeaver(beanClassLoader);
}
else {
throw new IllegalStateException("No LoadTimeWeaver available");
}
}
weaverToUse.addTransformer(new AspectJClassBypassingClassFileTransformer(
new ClassPreProcessorAgentAdapter()));
}
```
weaverToUse 这个参数就是被容器自动注入的 loadTimeWeaver bean,从 bean 定义 XML 中可以知道这个 bean 是 DefaultContextLoadTimeWeaver 类型的,它的
addTransformer 方法代码如下:
```java
public void addTransformer(ClassFileTransformer transformer) {
this.loadTimeWeaver.addTransformer(transformer);
}
```
DefaultContextLoadTimeWeaver 类也有个 loadTimeWeaver 属性,这个属性是在 setBeanClassLoader 方法中设置进去的:
```java
public void setBeanClassLoader(ClassLoader classLoader) {
LoadTimeWeaver serverSpecificLoadTimeWeaver = createServerSpecificLoadTimeWeaver(classLoader);
if (serverSpecificLoadTimeWeaver != null) {
if (logger.isInfoEnabled()) {
logger.info("Determined server-specific load-time weaver: " +
serverSpecificLoadTimeWeaver.getClass().getName());
}
this.loadTimeWeaver = serverSpecificLoadTimeWeaver;
}
else if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {
logger.info("Found Spring's JVM agent for instrumentation");
this.loadTimeWeaver = new InstrumentationLoadTimeWeaver(classLoader);
}
else {
try {
this.loadTimeWeaver = new ReflectiveLoadTimeWeaver(classLoader);
logger.info("Using a reflective load-time weaver for class loader: " +
this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName());
}
catch (IllegalStateException ex) {
throw new IllegalStateException(ex.getMessage() + " Specify a custom LoadTimeWeaver or start your " +
"Java virtual machine with Spring's agent: -javaagent:org.springframework.instrument.jar");
}
}
}
```
在方法里面判断了当前是否存在 Instrumentation 实例,最终会取 InstrumentationSavingAgent 类中的 instrumentation 的静态属性,判断这个属性是否是
null,从前面的分析可以知道 InstrumentationSavingAgent 这个类是 spring-instrument-3.2.9.RELEASE.jar 的代理入口类,当应用程序启动时启动了
spring-instrument-3.2.9.RELEASE.jar 代理时,即在虚拟机参数中设置了 -javaagent 参数,虚拟机会创建 Instrumentation 实例并传递给 premain
方法,InstrumentationSavingAgent 会把这个类保存在 instrumentation 静态属性中。所以在程序启动时启动了代理时
InstrumentationLoadTimeWeaver.isInstrumentationAvailable() 这个方法是返回 true 的,所以 loadTimeWeaver 属性会设置成
InstrumentationLoadTimeWeaver 对象。
接下来就看看 InstrumentationLoadTimeWeaver 类的 addTransformer 方法代码:
```java
public void addTransformer(ClassFileTransformer transformer) {
Assert.notNull(transformer, "Transformer must not be null");
FilteringClassFileTransformer actualTransformer =
new FilteringClassFileTransformer(transformer, this.classLoader);
synchronized (this.transformers) {
if (this.instrumentation == null) {
throw new IllegalStateException(
"Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.");
}
this.instrumentation.addTransformer(actualTransformer);
this.transformers.add(actualTransformer);
}
}
```
从代码中可以看到,这个方法中,把类转换器 actualTransformer 通过 instrumentation 实例注册给了虚拟机。这里采用了修饰器模式,actualTransformer 对
transformer 进行修改封装,下面是 FilteringClassFileTransformer 这个内部类的代码:
```java
private static class FilteringClassFileTransformer implements ClassFileTransformer {
private final ClassFileTransformer targetTransformer;
private final ClassLoader targetClassLoader;
public FilteringClassFileTransformer(ClassFileTransformer targetTransformer, ClassLoader targetClassLoader) {
this.targetTransformer = targetTransformer;
this.targetClassLoader = targetClassLoader;
}
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!this.targetClassLoader.equals(loader)) {
return null;
}
return this.targetTransformer.transform(
loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
@Override
public String toString() {
return "FilteringClassFileTransformer for: " + this.targetTransformer.toString();
}
}
```
这里面的 targetClassLoader 就是容器的 bean 类加载,在进行类字节码转换之前先判断执行类加载的加载器是否是 bean 类加载器,如果不是的话跳过类装换逻辑直接返回
null,返回 null 的意思就是不执行类转换还是使用原始的类字节码。什么情况下会有类加载不是 bean 的类加载器的情况?在我们上面列出的
AbstractApplicationContext 的 prepareBeanFactory 方法中有一行代码:
```java
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
```
当容器中注册了 loadTimeWeaver 之后会给容器设置一个 ContextTypeMatchClassLoader 类型的临时类加载器,在织入切面时只有在 bean
实例化时织入切面才有意义,在进行一些类型比较或者校验的时候,比如判断一个 bean 是否是
FactoryBean、BPP、BFPP,这时候不涉及到实例化,所以做字节码转换没有任何意义,而且还会增加无谓的性能消耗,所以在进行这些类型比较时使用这个临时的类加载器执行类加载,这样在上面的
transform 方法就会因为类加载不匹配而跳过字节码转换,这里有一点非常关键的是,ContextTypeMatchClassLoader 的父类加载就是容器 bean 类加载器,所以
ContextTypeMatchClassLoader 类加载器是不遵循“双亲委派”的,因为如果它遵循了“双亲委派”,那么它的类加载工作还是会委托给 bean 类加载器,这样的话 if
里面的条件就不会匹配,还是会执行类转换。ContextTypeMatchClassLoader 的类加载工作会委托给 ContextOverridingClassLoader 类对象,有兴趣可以看看
ContextOverridingClassLoader 和 OverridingClassLoader 这两个类的代码。
这个临时的类加载器会在容器初始化快结束时,容器 bean 实例化之前被清掉,代码在 AbstractApplicationContext 类的 finishBeanFactoryInitialization
方法:
```java
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
...
beanFactory.setTempClassLoader(null);
// Allow for caching all bean definition metadata, not expecting further changes.
beanFactory.freezeConfiguration();
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
}
```
再回头来看 FilteringClassFileTransformer 类的 transform 方法,调用 targetTransformer 执行字节码转换。来看看 targetTransformer
这个类转换器是在哪创建的,回头再看下 AspectJWeavingEnabler 类的 enableAspectJWeaving 方法,有下面这行代码:
```java
weaverToUse.addTransformer(new AspectJClassBypassingClassFileTransformer(
new ClassPreProcessorAgentAdapter()));
```
AspectJClassBypassingClassFileTransformer 类和 ClassPreProcessorAgentAdapter 类都实现了字节码转换接口 ClassFileTransformer:
```java
private static class AspectJClassBypassingClassFileTransformer implements ClassFileTransformer {
private final ClassFileTransformer delegate;
public AspectJClassBypassingClassFileTransformer(ClassFileTransformer delegate) {
this.delegate = delegate;
}
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.startsWith("org.aspectj") || className.startsWith("org/aspectj")) {
return classfileBuffer;
}
return this.delegate.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
}
```
这也是一个修饰器模式,最终会调用 ClassPreProcessorAgentAdapter 的 transform 方法执行字节码转换逻辑,在类加载器定义类时(即调用 defineClass
方法)会调用此类的 transform 方法来进行字节码转换替换原始类。ClassPreProcessorAgentAdapter 类中的代码比较多,这里就不列出来了,它的主要工作是解析
aop.xml 文件,解析类中的 Aspect 注解,并且根据解析结果来生成转换后的字节码。
在上面例子里面提到的通过 context 名称空间下的 load-time-weaver 标签来配置,其本质原理是一致的。通过在 context 的名称空间处理器
ContextNamespaceHandler 中可以看到 load-time-weaver 标签的解析器是 LoadTimeWeaverBeanDefinitionParser 类,看下这个类的代码:
```java
class LoadTimeWeaverBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
private static final String WEAVER_CLASS_ATTRIBUTE = "weaver-class";
private static final String ASPECTJ_WEAVING_ATTRIBUTE = "aspectj-weaving";
private static final String DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME =
"org.springframework.context.weaving.DefaultContextLoadTimeWeaver";
private static final String ASPECTJ_WEAVING_ENABLER_CLASS_NAME =
"org.springframework.context.weaving.AspectJWeavingEnabler";
@Override
protected String getBeanClassName(Element element) {
if (element.hasAttribute(WEAVER_CLASS_ATTRIBUTE)) {
return element.getAttribute(WEAVER_CLASS_ATTRIBUTE);
}
return DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME;
}
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) {
return ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME;
}
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
if (isAspectJWeavingEnabled(element.getAttribute(ASPECTJ_WEAVING_ATTRIBUTE), parserContext)) {
RootBeanDefinition weavingEnablerDef = new RootBeanDefinition();
weavingEnablerDef.setBeanClassName(ASPECTJ_WEAVING_ENABLER_CLASS_NAME);
parserContext.getReaderContext().registerWithGeneratedName(weavingEnablerDef);
if (isBeanConfigurerAspectEnabled(parserContext.getReaderContext().getBeanClassLoader())) {
new SpringConfiguredBeanDefinitionParser().parse(element, parserContext);
}
}
}
protected boolean isAspectJWeavingEnabled(String value, ParserContext parserContext) {
if ("on".equals(value)) {
return true;
}
else if ("off".equals(value)) {
return false;
}
else {
// Determine default...
ClassLoader cl = parserContext.getReaderContext().getResourceLoader().getClassLoader();
return (cl.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) != null);
}
}
protected boolean isBeanConfigurerAspectEnabled(ClassLoader beanClassLoader) {
return ClassUtils.isPresent(SpringConfiguredBeanDefinitionParser.BEAN_CONFIGURER_ASPECT_CLASS_NAME,
beanClassLoader);
}
}
```
从上面的代码可以看出在解析 load-time-weaver 标签时,从 getBeanClassName 方法中可以看到,如果没有指定 weaver-class 属性,会自动给容器中注入一个
org.springframework.context.weaving.DefaultContextLoadTimeWeaver 类型的 bean,从 resolveId 方法中看到,该 bean 的名称为 loadTimeWeaver。在
doParse 方法中,还会注册一个类型为 org.springframework.context.weaving.AspectJWeavingEnabler 的匿名 bean。从此可以看出下面两段配置完全是等价的:
```xml
```
```xml
```
## [Redis Sentinel:实现高可用性与故障转移](https://blog.dong4j.site/posts/a87e6e25.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
使用 Redis Sentinel 重构现有架构
> 对于搭建高可用 Redis 服务,网上已有了很多方案,例如 Keepalived,Codis,Twemproxy,Redis Sentinel。
> 其中 Codis 和 Twemproxy 主要是用于大规模的 Redis 集群中,也是在 Redis 官方发布 Redis Sentinel 之前 豌豆荚 和 twitter 提供的开源解决方案。
> Redis Sentinel 可以理解为一个监控 Redis Server 服务是否正常的进程,并且一旦检测到不正常,可以自动地将备份 (slave)Redis Server 启用,使得外部用户对
> Redis 服务内部出现的异常无感知。
## 原有架构
![20241229154732_wkmt6CrY.webp](https://cdn.dong4j.site/source/image/20241229154732_wkmt6CrY.webp)
### 存在的问题
1. 配置部署复杂
2. 不稳定
## Redis Sentinel 高可用
![20241229154732_g8KXigVF.webp](https://cdn.dong4j.site/source/image/20241229154732_g8KXigVF.webp)
下面以 1 个主节点、2 个从节点、3 个 Sentinel 节点组成的 Redis Sentinel 为例子
**故障转移处理逻辑:**
1. 主节点出现故障, 此时两个从节点与主节点时区连接, 主从复制失败;
![20241229154732_yV32SN5g.webp](https://cdn.dong4j.site/source/image/20241229154732_yV32SN5g.webp)
2. 每个 Sentinel 节点通过定期监控发现主节点出现故障;
![20241229154732_dmt2MfIR.webp](https://cdn.dong4j.site/source/image/20241229154732_dmt2MfIR.webp)
3. 多个 Sentinel 节点对主节点的故障达成一致, 选举出 sentinel-3 节点作为领导者负责故障转移;
![20241229154732_ypMc084g.webp](https://cdn.dong4j.site/source/image/20241229154732_ypMc084g.webp)
1. 原来的从节点 slave-1 成为新的主节点后, 更新应用方的主节点信息;
2. 客户端命令另一个从节点 slave-2 去复制新的主节点;
3. 待原来的主节点恢复后, 让它去复制新的主节点;
![20241229154732_ZvDNSolX.webp](https://cdn.dong4j.site/source/image/20241229154732_ZvDNSolX.webp)
4. 故障转移后的结构图
![20241229154732_8ZiZco7l.webp](https://cdn.dong4j.site/source/image/20241229154732_8ZiZco7l.webp)
### Redis Sentinel 功能
1. **监控:** Sentinel 节点会定期检测 Redis 数据节点和其余 Sentinel 节点是否可达;
2. **通知:** Sentinel 节点会将故障转移的结果通知给应用方;
3. **主节点故障转移:** 实现从节点晋升为主节点并维护后续正确的主从关系;
4. **配置提供者:** 客户端在初始化时, 连接 Sentinel 节点集群, 从中获取主节点信息;
采用多个 Sentinel 节点的优点:
1. 对于节点故障判断由多个 Sentinel 节点共同完成, 有效防止误判;
2. 避免单点故障;
### 几个概念
#### 三个定时监控任务
1. 每隔 10 秒. 每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构;
![20241229154732_g9I5S7TW.webp](https://cdn.dong4j.site/source/image/20241229154732_g9I5S7TW.webp)
2. 每隔 2 秒, 每个 Sentinel 节点向 _sentinel_:hello 频道上发送该 Sentinel 节点对于主节点的判断一级当前 Sentinel 节点的信息, 同时每个
Sentinel 节点也会订阅该频道;
![20241229154732_ug9RgBvu.webp](https://cdn.dong4j.site/source/image/20241229154732_ug9RgBvu.webp)
3. 每隔 1 秒, 每个 Sentine 会向主从节点, 其他 Sentinel 节点发送 ping 命令做心跳检测, 来确保节点当前是否可达
![20241229154732_OuXnSfQM.webp](https://cdn.dong4j.site/source/image/20241229154732_OuXnSfQM.webp)
#### 主观下线(Subjectively Down, 简称 SDOWN)
> 指的是单个 Sentinel 实例对服务器做出的下线判断。
每个 Sentinel 节点会每隔 1 秒对主节 点、从节点、其他 Sentinel 节点发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds
没有进行有效回复,Sentinel 节点就会对该节点做失败 判定,这个行为叫做主观下线
![20241229154732_7Uguxxes.webp](https://cdn.dong4j.site/source/image/20241229154732_7Uguxxes.webp)
#### 客观下线(Objectively Down, 简称 ODOWN)
> 指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。
> (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)
## Redis Sentinel 安装与部署
下面将以 3 个 Sentinel 节点、1 个主节点、2 个从节点组成一个 Redis Sentinel 进行说明
![20241229154732_ZsDsOhqv.webp](https://cdn.dong4j.site/source/image/20241229154732_ZsDsOhqv.webp)
具体的物理部署:
| 角色 | ip | port | 别名 |
| :--------- | :-------- | :---- | :--------- |
| master | 127.0.0.1 | 6379 | 主节点 |
| slave-1 | 127.0.0.1 | 6380 | slave-1 |
| slave-2 | 127.0.0.1 | 6381 | slave-2 |
| sentinel-1 | 127.0.0.1 | 26379 | sentinel-1 |
| sentinel-2 | 127.0.0.1 | 26380 | sentinel-2 |
| sentinel-3 | 127.0.0.1 | 26381 | sentinel-3 |
**1. master 配置**
```lua
daemonize yes
dbfilename "6379.db"
dir "/Users/codeai/Develop/logs/redis/db/"
logfile "/Users/codeai/Develop/logs/redis/log/6379.log"
port 6379
requirepass 1234
```
**2. slave-1 配置**
```lua
daemonize yes
dbfilename "6380.db"
dir "/Users/codeai/Develop/logs/redis/db/"
logfile "/Users/codeai/Develop/logs/redis/log/6380.log"
port 6380
slaveof 127.0.0.1 6379
# 设置 master 验证密码
masterauth 1234
# 设置 slave 密码
requirepass 1234
```
**3. slave-2 配置**
```lua
daemonize yes
dbfilename "6381.db"
dir "/Users/codeai/Develop/logs/redis/db/"
logfile "/Users/codeai/Develop/logs/redis/log/6381.log"
port 6381
slaveof 127.0.0.1 6379
# 设置 master 验证密码
masterauth 1234
# 设置 slave 密码
requirepass 1234
```
**4. 启动 redis 服务**
```lua
redis-server redis-6379.conf; redis-server redis-6380.conf; redis-server redis-6381.conf
```
![20241229154732_MofUCgDd.webp](https://cdn.dong4j.site/source/image/20241229154732_MofUCgDd.webp)
**5. 确认主从关系**
主节点视角
```lua
redis-cli -h 127.0.0.1 -p 6379 info replication
# Replication
# 主节点
role:master
# 有 2 个 从节点
connected_slaves:2
# 从节点 1 信息
slave0:ip=127.0.0.1,port=6380,state=online,offset=112,lag=0
# 从节点 2 信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=112,lag=0
master_replid:8f43dc48cca779f46fa1516a38e24fb8c5423d94
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:112
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:112
```
从节点视角
```lua
redis-cli -h 127.0.0.1 -p 6380 info replication
# Replication
# 从节点
role:slave
# 主节点信息
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:238
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:8f43dc48cca779f46fa1516a38e24fb8c5423d94
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:238
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:238
```
**6. 部署 Sentinel 节点**
```lua
port 26379
daemonize yes
logfile "/Users/codeai/Develop/logs/redis/log/26379.log"
dir "/Users/codeai/Develop/logs/redis/db/"
# 监控 127.0.0.1:6379 主节点; 2 表示判断主节点失败至少需要 2 个 sentinel 节点同意
sentinel monitor mymaster 127.0.0.1 6379 2
# 设置监听的 master 密码
sentinel auth-pass mymaster 1234
# 30 秒内 ping 失败, sentinel 则认为 master 不可用
sentinel down-after-milliseconds mymaster 30000
# 在发生 failover 主备切换时,这个选项指定了最多可以有多少个 slave 同时对新的 master 进行同步
sentinel parallel-syncs mymaster 1
# 如果在该时间(ms)内未能完成 failover 操作,则认为该 failover 失败
sentinel failover-timeout mymaster 180000
```
其他节点只是端口不同
**7. 启动 sentinel 节点**
```lua
# 方式一
redis-sentinel redis-sentinel-26379.conf; redis-sentinel redis-sentinel-26380.conf; redis-sentinel redis-sentinel-26381.conf
# 方式二
redis-server redis-sentinel-26379.conf --sentinel; redis-server redis-sentinel-26380.conf --sentinel; redis-server redis-sentinel-26381.conf --sentinel;
```
![20241229154732_zCpl5IS6.webp](https://cdn.dong4j.site/source/image/20241229154732_zCpl5IS6.webp)
**8. 确认关系**
```lua
redis-cli -h 127.0.0.1 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
```
## 部署方案
1. Sentinel 部署在不同物理机上;
2. 部署至少三个且奇数个的 Sentinel 节点;
### 一套 Sentinel 监控所有主节点
![20241229154732_iGgNGxWd.webp](https://cdn.dong4j.site/source/image/20241229154732_iGgNGxWd.webp)
优点:
1. 维护成本低
缺点:
1. 集群出现异常, 将导致服务不可用
2. 过多的网络连接
### 每个主节点各一套 Sentinel
![20241229154732_Co7PVXld.webp](https://cdn.dong4j.site/source/image/20241229154732_Co7PVXld.webp)
优点:
1. 某个 Sentinel 集群出现故障, 不会影响其他业务
2. 网络连接少
缺点:
1. 维护成本高
> 如果监控同一个业务的多个主节点集合, 推荐使用方案一
> 如果是多个业务不同主节点集合, 推荐方案二 (推荐)
## Redis 连接数太多导致
### 原因分析
Redis 默认最大连接数为 10000 个
1. 网络通信差, 按照 TCP 协议,客户端断开连接时,向服务器端发送 FIN 信号,但是服务端未接收到,客户端超时后放弃等待,直接断开,服务端由于通信故障,保持了
ESTABLISHED 状态;
2. 客户端异常, 客户端连接之后,由于代码运行过程中产生异常,导致未正常释放或者关闭连接;
3. client 设置不合理 (`client 数 * maxTotal` 是不能超过 redis 的最大连接数)
### 解决方案
**1. 修改 redis.config 配置**
```
# 连接的空闲实现超过 360s, 则主动关闭连接; 默认配置为 0 , 导致所有空闲 idle 连接未被释放, 服务端连接泄漏
timeout 360
# 默认关闭, 导致服务端不知客户端连接状态; 开启长连接, 服务端主动 (60s) 探测客户端 socket 状态
tcp-keeplive 60
```
**2. 完善代码**
> 客户端每次执行完 jedis 里面的方法之后必须关闭链接,释放资源
**3. redis-proxy 服务化**
---
## 重构方案
> 使用 Redis Sentinel 代替 Redis + Keepalived
### 架构
重构 redis-proxy
提供 6 种 连接模式
![20241229154732_smjFqOxg.webp](https://cdn.dong4j.site/source/image/20241229154732_smjFqOxg.webp)
### component-redis 介绍
项目中有单独使用 Redis 的, 也有使用分片方式连接的, 也有使用 redis-proxy 组件来连接 Redis 的.
造成代码不好管理, 因此使用 component-redis 组件为其他模块提供连接 Redis 的功能, 统一管理 Redis 相关代码.
作为连接 Redis 的工具模块, 为其他模块提供操作 Redis 的功能, 具有多种模式选择.
客户端不需要再写连接 Redis 相关代码, 只需要按照要求配置即可, 减少了冗余代码;
兼容原有代码, 只需要将 redis-proxy 依赖替换成 component-redis 即可使用.
#### 功能
1. 多种模式任君选择 (standalone, sentinel, shard, shard-sentinel, cluster, hybrid);
2. 使用简单, 引入 jar 即可使用;
3. 扩展方便, 如果 `RedisService` 满足不了现有业务需要, 可直接使用各种 Pool 获取 jedis, shardedJedis, jedisCluster 自由发挥;
#### 使用方式
##### 1. 引入依赖
```xml
com.xxxx.msearch
component-redis
最新版本
```
##### 2. 配置
```lua
# jedis pool 配置
# 连接超时时间(毫秒)
redis.connectionTimeout=2000
# 等待 Response 超时时间 (新增)
redis.soTimeout=5000
redis.pool.maxActive=5000
redis.pool.maxWait=5000
redis.pool.maxIdle=200
redis.pool.minIdleTime=120
redis.pool.testOnBorrow=true
redis.pool.testOnReturn=false
# Redis 配置
redis.model=standalone
redis.node=redis://127.0.0.1:6379
```
##### 3. 注入 service
```java
@Autowired
private RedisService redisService;
@Test
public void testRedisService() throws Exception {
redisService.set("redisServiceTest", "redisServiceTest");
}
```
### component-redis 配置规范
> 由于 component-redis 组件需要支持多种模式, 配置需要规范的格式才能避免出错.
1. 节点使用 `,` 分隔, 模式分组使用 `;` 分隔;
2. 使用某个 Redis 时, 不要把所有配置全部加上;
jedisPool 配置是固定配置, 每种模式都会使用到, 只需要根据业务调整即可.
```lua
## 连接超时时间(毫秒)
redis.connectionTimeout=2000
# 等待 Response 超时时间 (新增)
redis.soTimeout=5000
# 连接池最大连接数(使用负值表示没有限制)
redis.pool.maxActive=5000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
redis.pool.maxWait=5000
# 连接池中的最大空闲连接
redis.pool.maxIdle=200
# 连接池中的最小空闲连接
redis.pool.minIdleTime=120
# 当调用 borrow Object 方法时,是否进行有效性检查
redis.pool.testOnBorrow=true
# 调用 return 一个对象方法时,是否检查其有效性
redis.pool.testOnReturn=false
```
```lua
redis.model= 模式名
redis.node= 业务名 #模式名://[:password@]ip:port[/database];...
```
这里以现有业务为例子:
```lua
redis.model=hybrid
# callout 使用单机模式, biz 使用哨兵模式
redis.node=callout#standalone://:1234@127.0.0.1:6382;biz#sentinel://:5678@127.0.0.1:26379,sentinel://127.0.0.1:26380,sentinel://127.0.0.1:26381
```
#### 配置优化
**使用标准的 uri 协议代替 host 和 port**
避免手动解析出错
格式如下:
```lua
# 完整格式
redis://user:password@ip:port/database
# 不需要用户名的格式
redis://:password@ip:port/database
# 不需要密码的格式
redis://ip:port/database
# 不需要 database 的配置, 将默认使用 0 db
redis://ip:port
```
**密码设置**
redis 的查询速度是非常快的,外部用户一秒内可以尝试多大 150K 个密码;所以密码要尽量长;
建议设置为 64 位长度密码
##### standalone (单机) 模式配置
```lua
redis.model=standalone
# password 前面的 : 不能少
redis.node=redis://:password@127.0.0.1:6382
```
demo:
```lua
redis.model=standalone
redis.node=redis://127.0.0.1:6379
```
##### sentinel
哨兵模式是对单机模式高可用的一种实现方式, 可以实现故障主从自动切换
哨兵模式需要配置 master name, 和 `redis-sentinel`.conf 中的 `sentinel monitor masterName xxx` 保持一致
哨兵模式只能接收一个密码, 密码设置在任意节点即可 (第一个最好了)
```properties
redis.model=sentinel
redis.node=mymaster#redis://:1234@127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381
```
哨兵模式就是单机模式的增强版, 需要配置多个哨兵节点 (避免造成主从切换失败, 最少 3 组哨兵), 节点之间使用 `,` 分隔
demo:
```lua
redis.model=sentinel
redis.node=mymaster#redis://:s4jRcLhAcUdKrNmqv9XQxwbEUZ6p4sK3kTFE4k9ts3PLahnswEzE4aPgXEQ6QdMa@127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381
```
##### sharding (分片) 模式配置
**每组 redis 实例可以设置不同的密码**
分片实例之间使用 `,` 分隔
```lua
redis.model=sharding
redis.node=redis://:1234@127.0.0.1:6382,redis://:5678@127.0.0.1:6382
```
demo:
```lua
# redis://:password@ip:port/database 没有密码则使用 redis://ip:port/database
redis.model=sharding
redis.node=redis://:1234@127.0.0.1:6382,redis://:s4jRcLhAcUdKrNmqv9XQxwbEUZ6p4sK3kTFE4k9ts3PLahnswEzE4aPgXEQ6QdMa@127.0.0.1:6379
```
##### sharding-sentinel
分片哨兵模式是对分片模式高可用的一种实现方式, 可以实现分片模式下, 故障主从自动切换
分片哨兵模式是哨兵模式和分片模式的结合, 配置可
demo:
```lua
redis.model=sharding-sentinel
redis.node=mymaster#redis://127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381;mymaster1#redis://127.0.0.1:26379,redis://127.0.0.1:26380,redis://127.0.0.1:26381
```
##### cluster
redis 3.x 远程集群模式
demo:
```lua
redis.model=cluster
redis.node=redis://127.0.0.1:6379,redis://127.0.0.1:6380,redis://127.0.0.1:6381
```
##### hybrid
混合模式, 为了兼容现有 Redis 环境, 一种临时的解决方案, 改造完成后, 将配置修改为 `sentinel` 即可
demo:
```lua
redis.model=hybrid
redis.node=callout#standalone://:1234@127.0.0.1:6382;mymaster#sentinel://:s4jRcLhAcUdKrNmqv9XQxwbEUZ6p4sK3kTFE4k9ts3PLahnswEzE4aPgXEQ6QdMa@127.0.0.1:26379,sentinel://127.0.0.1:26380,sentinel://127.0.0.1:26381
```
混合模式实现了 `standalone`, `sentinel`, `sharding` 模式的混合使用
### 代码重构
1. 使用 `@Value` 实现自动配置, 代替 xml 中的 bean 标签;
2. 使用 logbok 简化代码;
3. 不在使用 RedisServiceFacotry 获取 redisService, 某个模块需要使用该组件时, import xml 即可 (多容器问题);
4. 使用 log4j2 代替 log4j;
5. 编译版本由 jdk1.6 改为 jdk1.7;
6. 使用代理类重构安全关闭 jedis 连接的方式;
重构前:
```java
@Override
public String set(String flag, final String key, final String value) throws Exception {
return new RedisCallBack() {
@Override
public String doCallback(Jedis jedis) {
return jedis.set(key, value);
}
}.callback(getJedisPoolByFlag(flag));
}
```
重构后:
```java
@Override
public String set(String flag, final String key, final String value) {
return RedisUtil.jedisProxy(model, flag).set(key, value);
}
```
## [Java开发者的痛:如何避免和解决常见的Jar包冲突问题](https://blog.dong4j.site/posts/a65dfb13.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
Jar 包冲突是老生常谈的问题,几乎每一个 Java 程序猿都不可避免地遇到过,并且也都能想到通常的原因一般是同一个 Jar 包由于 maven 传递依赖等原因被引进了多个不同的版本而导致,可采用依赖排除、依赖管理等常规方式来尝试解决该问题,但这些方式真正能彻底解决该冲突问题吗?答案是否定的。笔者之所以将文章题目起为 “重新看待”,是因为之前对于 Jar 包冲突问题的理解仅仅停留在前面所说的那些,直到在工作中遇到的一系列 Jar 包冲突问题后,才发现并不是那么简单,对该问题有了重新的认识,接下来本文将围绕 Jar 包冲突的问题本质和相关的解决方案这两个点进行阐述。
# Jar 包冲突问题
## 一、冲突的本质
Jar 包冲突的本质是什么?Google 了半天也没找到一个让人满意的完整定义。其实,我们可以从 Jar 包冲突产生的结果来总结,在这里给出如下定义(此处如有不妥,欢迎拍砖 -):
> **Java 应用程序因某种因素,加载不到正确的类而导致其行为跟预期不一致。**
具体来说可分为两种情况:1)应用程序依赖的同一个 Jar 包出现了多个不同版本,并选择了错误的版本而导致 JVM 加载不到需要的类或加载了错误版本的类,为了叙述的方便,笔者称之为**第一类 Jar 包冲突问题**;2)同样的类(类的全限定名完全一样)出现在多个不同的依赖 Jar 包中,即该类有多个版本,并由于 Jar 包加载的先后顺序导致 JVM 加载了错误版本的类,称之为**第二类 Jar 包问题**。这两种情况所导致的结果其实是一样的,都会使应用程序加载不到正确的类,那其行为自然会跟预期不一致了,以下对这两种类型进行详细分析。
### 1.1 同一个 Jar 包出现了多个不同版本
随着 Jar 包迭代升级,我们所依赖的开源的或公司内部的 Jar 包工具都会存在若干不同的版本,而版本升级自然就避免不了类的方法签名变更,甚至于类名的更替,而我们当前的应用程序往往依赖特定版本的某个类 **M** ,由于 maven 的传递依赖而导致同一个 Jar 包出现了多个版本,当 maven 的仲裁机制选择了错误的版本时,而恰好类 **M** 在该版本中被去掉了,或者方法签名改了,导致应用程序因找不到所需的类 **M** 或找不到类 **M** 中的特定方法,就会出现第一类 Jar 冲突问题。可总结出该类冲突问题发生的以下三个必要条件:
- 由于 maven 的传递依赖导致依赖树中出现了同一个 Jar 包的多个版本
- 该 Jar 包的多个版本之间存在接口差异,如类名更替,方法签名更替等,且应用程序依赖了其中有变更的类或方法
- maven 的仲裁机制选择了错误的版本
### 1.2 同一个类出现在多个不同 Jar 包中
同样的类出现在了应用程序所依赖的两个及以上的不同 Jar 包中,这会导致什么问题呢?我们知道,同一个类加载器对于同一个类只会加载一次(多个不同类加载器就另说了,这也是解决 Jar 包冲突的一个思路,后面会谈到),那么当一个类出现在了多个 Jar 包中,假设有 **A** 、 **B** 、 **C** 等,由于 Jar 包依赖的路径长短、声明的先后顺序或文件系统的文件加载顺序等原因,类加载器首先从 Jar 包 **A** 中加载了该类后,就不会加载其余 Jar 包中的这个类了,那么问题来了:如果应用程序此时需要的是 Jar 包 **B** 中的类版本,并且该类在 Jar 包 **A** 和 **B** 中有差异(方法不同、成员不同等等),而 JVM 却加载了 Jar 包 **A** 的中的类版本,与期望不一致,自然就会出现各种诡异的问题。
从上面的描述中,可以发现出现不同 Jar 包的冲突问题有以下三个必要条件:
- 同一个类 **M** 出现在了多个依赖的 Jar 包中,为了叙述方便,假设还是两个: **A** 和 **B**
- Jar 包 **A** 和 **B** 中的该类 **M** 有差异,无论是方法签名不同也好,成员变量不同也好,只要可以造成实际加载的类的行为和期望不一致都行。如果说 Jar 包 **A** 和 **B** 中的该类完全一样,那么类加载器无论先加载哪个 Jar 包,得到的都是同样版本的类 **M** ,不会有任何影响,也就不会出现 Jar 包冲突带来的诡异问题。
- 加载的类 **M** 不是所期望的版本,即加载了错误的 Jar 包
## 二、冲突的产生原因
### 2.1 maven 仲裁机制
当前 maven 大行其道,说到第一类 Jar 包冲突问题的产生原因,就不得不提 [maven 的依赖机制](https://link.jianshu.com/?t=https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html)了。传递性依赖是 Maven2.0 引入的新特性,让我们只需关注直接依赖的 Jar 包,对于间接依赖的 Jar 包,Maven 会通过解析从远程仓库获取的依赖包的 pom 文件来隐式地将其引入,这为我们开发带来了极大的便利,但与此同时,也带来了常见的问题——版本冲突,即同一个 Jar 包出现了多个不同的版本,针对该问题 Maven 也有一套仲裁机制来决定最终选用哪个版本,但** Maven 的选择往往不一定是我们所期望的**,这也是产生 Jar 包冲突最常见的原因之一。先来看下 Maven 的仲裁机制:
- 优先按照依赖管理元素中指定的版本声明进行仲裁,此时下面的两个原则都无效了
- 若无版本声明,则按照 “短路径优先” 的原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本
- 若路径长度一致,则按照 “第一声明优先” 的原则进行仲裁,即选择 POM 中最先声明的版本
从 maven 的仲裁机制中可以发现,除了第一条仲裁规则(这也是解决 Jar 包冲突的常用手段之一)外,后面的两条原则,对于同一个 Jar 包不同版本的选择,maven 的选择有点 “一厢情愿” 了,也许这是 maven 研发团队在总结了大量的项目依赖管理经验后得出的两条结论,又或者是发现根本找不到一种统一的方式来满足所有场景之后的无奈之举,可能这对于多数场景是适用的,但是**它不一定适合我——当前的应用**,因为每个应用都有其特殊性,该依赖哪个版本,maven 没办法帮你完全搞定,如果你没有规规矩矩地使用来进行依赖管理,就注定了逃脱不了第一类 Jar 包冲突问题。
### 2.1 Jar 包的加载顺序
对于第二类 Jar 包冲突问题,即多个不同的 Jar 包有类冲突,这相对于第一类问题就显得更为棘手。为什么这么说呢?在这种情况下,两个不同的 Jar 包,假设为 **A**、 **B**,它们的名称互不相同,甚至可能完全不沾边,如果不是出现冲突问题,你可能都不会发现它们有共有的类!对于 A、B 这两个 Jar 包,maven 就显得无能为力了,因为 maven 只会为你针对同一个 Jar 包的不同版本进行仲裁,而这俩是属于不同的 Jar 包,超出了 maven 的依赖管理范畴。此时,当 A、B 都出现在应用程序的类路径下时,就会存在潜在的冲突风险,即 A、B 的加载先后顺序就决定着 JVM 最终选择的类版本,如果选错了,就会出现诡异的第二类冲突问题。
那么 Jar 包的加载顺序都由哪些因素决定的呢?具体如下:
- Jar 包所处的加载路径,或者换个说法就是加载该 Jar 包的类加载器在 JVM 类加载器树结构中所处层级。由于 JVM 类加载的双亲委派机制,层级越高的类加载器越先加载其加载路径下的类,顾名思义,引导类加载器(bootstrap ClassLoader,也叫启动类加载器)是最先加载其路径下 Jar 包的,其次是扩展类加载器(extension ClassLoader),再次是系统类加载器(system ClassLoader,也就是应用加载器 appClassLoader),Jar 包所处加载路径的不同,就决定了它的加载顺序的不同。比如我们在 eclipse 中配置 web 应用的 resin 环境时,对于依赖的 Jar 包是添加到`Bootstrap Entries`中还是`User Entries`中呢,则需要仔细斟酌下咯。
- 文件系统的文件加载顺序。这个因素很容易被忽略,而往往又是因环境不一致而导致各种诡异冲突问题的罪魁祸首。因 tomcat、resin 等容器的 ClassLoader 获取加载路径下的文件列表时是不排序的,这就依赖于底层文件系统返回的顺序,那么当不同环境之间的文件系统不一致时,就会出现有的环境没问题,有的环境出现冲突。例如,对于 Linux 操作系统,返回顺序则是由 iNode 的顺序来决定的,如果说测试环境的 Linux 系统与线上环境不一致时,就极有可能出现典型案例:测试环境怎么测都没问题,但一上线就出现冲突问题,规避这种问题的最佳办法就是尽量保证测试环境与线上一致。
## 三、冲突的表象
Jar 包冲突可能会导致哪些问题?通常发生在编译或运行时,主要分为两类问题:一类是比较直观的也是最为常见的错误是抛出各种运行时异常,还有一类就是比较隐晦的问题,它不会报错,其表现形式是应用程序的行为跟预期不一致,分条罗列如下:
- **java.lang.ClassNotFoundException**,即 java 类找不到。这类典型异常通常是由于,没有在依赖管理中声明版本,maven 的仲裁的时候选取了错误的版本,而这个版本缺少我们需要的某个 class 而导致该错误。例如 httpclient-4.4.jar 升级到 httpclient-4.36.jar 时,类 org.apache.http.conn.ssl.NoopHostnameVerifier 被去掉了,如果此时我们本来需要的是 4.4 版本,且用到了 NoopHostnameVerifier 这个类,而 maven 仲裁时选择了 4.6,则会导致 ClassNotFoundException 异常。
- **java.lang.NoSuchMethodError**,即找不到特定方法,第一类冲突和第二类冲突都可能导致该问题——加载的类不正确。若是第一类冲突,则是由于错误版本的 Jar 包与所需要版本的 Jar 包中的类接口不一致导致,例如 antlr-2.7.2.jar 升级到 antlr-2.7.6.Jar 时,接口 antlr.collections.AST.getLine() 发生变动,当 maven 仲裁选择了错误版本而加载了错误版本的类 AST,则会导致该异常;若是第二类冲突,则是由于不同 Jar 包含有的同名类接口不一致导致,**典型的案例**:Apache 的 commons-lang 包,2.x 升级到 3.x 时,包名直接从 commons-lang 改为 commons-lang3,部分接口也有所改动,由于包名不同和传递性依赖,经常会出现两种 Jar 包同时在 classpath 下,org.apache.commons.lang.StringUtils.isBlank 就是其中有差异的接口之一,由于 Jar 包的加载顺序,导致加载了错误版本的 StringUtils 类,就可能出现 NoSuchMethodError 异常。
- **java.lang.NoClassDefFoundError**,**java.lang.LinkageError** 等,原因和上述雷同,就不作具体案例分析了。
- **没有报错异常,但应用的行为跟预期不一致**。这类问题同样也是由于运行时加载了错误版本的类导致,但跟前面不同的是,冲突的类接口都是一致的,但具体实现逻辑有差异,当我们加载的类版本不是我们需要的实现逻辑,就会出现行为跟预期不一致问题。这类问题通常发生在我们自己内部实现的多个 Jar 包中,由于包路径和类名命名不规范等问题,导致两个不同的 Jar 包出现了接口一致但实现逻辑又各不相同的同名类,从而引发此问题。
# 解决方案
## 一、问题排查和解决
1. 如果有异常堆栈信息,根据错误信息即可定位导致冲突的类名,然后在 eclipse 中`CTRL+SHIFT+T`或者在 idea 中`CTRL+N`就可发现该类存在于多个依赖 Jar 包中
2. 若步骤 1 无法定位冲突的类来自哪个 Jar 包,可在应用程序启动时加上 JVM 参数`-verbose:class`或者`-XX:+TraceClassLoading`,日志里会打印出每个类的加载信息,如来自哪个 Jar 包
3. 定位了冲突类的 Jar 包之后,通过`mvn dependency:tree -Dverbose -Dincludes=:`查看是哪些地方引入的 Jar 包的这个版本
4. 确定 Jar 包来源之后,如果是第一类 Jar 包冲突,则可用 **排除不需要的 Jar 包版本或者在依赖管理** 中申明版本;若是第二类 Jar 包冲突,如果可排除,则用排掉不需要的那个 Jar 包,若不能排,则需考虑 Jar 包的升级或换个别的 Jar 包。当然,除了这些方法,还可以从类加载器的角度来解决该问题,可参考博文——[如果 jar 包冲突不可避免,如何实现 jar 包隔离](https://www.shop988.com/blog/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0jar%E5%8C%85%E9%9A%94%E7%A6%BB.html),其思路值得借鉴。
## 二、有效避免
从上一节的解决方案可以发现,当出现第二类 Jar 包冲突,且冲突的 Jar 包又无法排除时,问题变得相当棘手,这时候要处理该冲突问题就需要较大成本了,所以,最好的方式是**在冲突发生之前能有效地规避之**!就好比数据库死锁问题,死锁避免和死锁预防就显得相当重要,若是等到真正发生死锁了,常规的做法也只能是回滚并重启部分事务,这就捉襟见肘了。那么怎样才能有效地规避 Jar 包冲突呢?
### 2.1 良好的习惯:依赖管理
对于第一类 Jar 包冲突问题,通常的做法是用排除不需要的版本,但这种做法带来的问题是每次引入带有传递性依赖的 Jar 包时,都需要一一进行排除,非常麻烦。maven 为此提供了集中管理依赖信息的机制,即依赖管理元素,对依赖 Jar 包进行统一版本管理,一劳永逸。通常的做法是,在 parent 模块的 pom 文件中尽可能地声明所有相关依赖 Jar 包的版本,并在子 pom 中简单引用该构件即可。
来看个示例,当开发时确定使用的 httpclient 版本为 4.5.1 时,可在父 pom 中配置如下:
```
4.5.1
org.apache.httpcomponents
httpclient
${httpclient.version}
```
然后各个需要依赖该 Jar 包的子 pom 中配置如下依赖:
```xml
dependencies>
org.apache.httpcomponents
httpclient
```
### 2.2 冲突检测插件
对于第二类 Jar 包冲突问题,前面也提到过,其核心在于同名类出现在了多个不同的 Jar 包中,如果人工来排查该问题,则需要逐个点开每个 Jar 包,然后相互对比看有没同名的类,那得多么浪费精力啊?!好在这种费时费力的体力活能交给程序去干。**maven-enforcer-plugin**,这个强大的 maven 插件,配合** extra-enforcer-rules** 工具,能自动扫描 Jar 包将冲突检测并打印出来,汗颜的是,笔者工作之前居然都没听过有这样一个插件的存在,也许是没遇到像工作中这样的冲突问题,算是涨姿势了。其原理其实也比较简单,通过扫描 Jar 包中的 class,记录每个 class 对应的 Jar 包列表,如果有多个即是冲突了,故不必深究,我们只需要关注如何用它即可。
在**最终需要打包运行的应用模块 pom** 中,引入 maven-enforcer-plugin 的依赖,在 build 阶段即可发现问题,并解决它。比如对于具有 parent pom 的多模块项目,需要将插件依赖声明在应用模块的 pom 中。这里有童鞋可能会疑问,为什么不把插件依赖声明在 parent pom 中呢?那样依赖它的应用子模块岂不是都能复用了?这里之所以强调 “打包运行的应用模块 pom”,是因为冲突检测针对的是最终集成的应用,关注的是应用运行时是否会出现冲突问题,而每个不同的应用模块,各自依赖的 Jar 包集合是不同的,由此而产生的列表也是有差异的,因此只能针对应用模块 pom 分别引入该插件。
先看示例用法如下:
```xml
org.apache.maven.plugins
maven-enforcer-plugin
1.4.1
enforce
enforce
enforce-ban-duplicate-classes
enforce
javax.*
org.junit.*
net.sf.cglib.*
org.apache.commons.logging.*
org.springframework.remoting.rmi.RmiInvocationHandler
true
true
org.codehaus.mojo
extra-enforcer-rules
1.0-beta-6
```
maven-enforcer-plugin 是通过很多预定义的标准规则([standard rules](https://link.jianshu.com/?t=http://maven.apache.org/enforcer/enforcer-rules/index.html))和用户自定义规则,来约束 maven 的环境因素,如 maven 版本、JDK 版本等等,它有很多好用的特性,具体可参见[官网](https://link.jianshu.com/?t=http://maven.apache.org/enforcer/maven-enforcer-plugin/)。而 Extra Enforcer Rules 则是* MojoHaus* 项目下的针对 maven-enforcer-plugin 而开发的提供额外规则的插件,这其中就包含前面所提的重复类检测功能,具体用法可参见[官网](https://link.jianshu.com/?t=http://www.mojohaus.org/extra-enforcer-rules/),这里就不详细叙述了。
# 典型案例
## 第一类 Jar 包冲突
这类 Jar 包冲突是最常见的也是相对比较好解决的,已经在[三、冲突的表象](https://www.jianshu.com/p/100439269148#%E4%B8%89%E3%80%81%E5%86%B2%E7%AA%81%E7%9A%84%E8%A1%A8%E8%B1%A1)这节中列举了部分案例,这里就不重复列举了。
## 第二类 Jar 包冲突
### Spring2.5.6 与 Spring3.x
Spring2.5.6 与 Spring3.x,从单模块拆分为多模块,Jar 包名称(artifactId)也从 spring 变为 spring-submoduleName,如
spring-context、spring-aop 等等,并且也有少部分接口改动(Jar 包升级的过程中,这也是在所难免的)。由于是不同的 Jar 包,经 maven 的传递依赖机制,就会经常性的存在这俩版本的 Spring 都在 classpath 中,从而引发潜在的冲突问题。
## [Java代码审查:常见错误及改进指南](https://blog.dong4j.site/posts/c43d4ea4.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
整理一下项目中不好的代码写法
![20241229154732_ZA3tXTde.webp](https://cdn.dong4j.site/source/image/20241229154732_ZA3tXTde.webp)
以下是一些具有代表性的问题, 都是一些一看就明白的问题, 还有一些代码的坑, 慢慢填吧.
只针对代码, 不针对谁, 如果写的不对的对方, 你咬我啊
## 代码问题
### 还失败重试? 失败重试个啥? 直接返回了 老铁!!
代码 1 后面, 获取了 batchResult, 不应该重新赋值 code 嘛?
代码 2 为修改后
![20241229154732_GNL6YDkH.webp](https://cdn.dong4j.site/source/image/20241229154732_GNL6YDkH.webp)
### Intellij idea 是个好东西
![20241229154732_l9cqCugh.webp](https://cdn.dong4j.site/source/image/20241229154732_l9cqCugh.webp)
![20241229154732_mOz94KIb.webp](https://cdn.dong4j.site/source/image/20241229154732_mOz94KIb.webp)
修改为:
![20241229154732_R03cnTDt.webp](https://cdn.dong4j.site/source/image/20241229154732_R03cnTDt.webp)
catch 里面使用 printStackTrace(), 错误日志全部输出到 `catalina.out`, 你考虑过 catalina 的感受吗?
**日志问题后面说**
### 这是先斩后奏吗?
前面都调用了 list 的 size 方法, 后面再来判断 list 是否为 null?
这种代码我看见起码不下 10 处, 系统能稳定吗老铁?
![20241229154732_poqachWZ.webp](https://cdn.dong4j.site/source/image/20241229154732_poqachWZ.webp)
idea 都知道的问题, 你不应该不知道
![20241229154732_pk5YyTrv.webp](https://cdn.dong4j.site/source/image/20241229154732_pk5YyTrv.webp)
logger.info 输出问题:
用 `log.info("{}", xxxx)`, 不要自己拼接字符串
### 老铁, 你可长点心吧
![20241229154732_mQUneZBa.webp](https://cdn.dong4j.site/source/image/20241229154732_mQUneZBa.webp)
### 老铁, 我就服你
![20241229154732_CjuLa8Xz.webp](https://cdn.dong4j.site/source/image/20241229154732_CjuLa8Xz.webp)
google : logger.error() 正确使用姿势
![20241229154732_EXMYUxyi.webp](https://cdn.dong4j.site/source/image/20241229154732_EXMYUxyi.webp)
### JDK7 之后的变化
JDK7 之 钻石语法
![20241229154732_2D69JJJC.webp](https://cdn.dong4j.site/source/image/20241229154732_2D69JJJC.webp)
![20241229154732_IgcfD8YG.webp](https://cdn.dong4j.site/source/image/20241229154732_IgcfD8YG.webp)
### 画蛇添足
value 就是 String 类型了, 写 toString() 是为了练打字吗?
![20241229154732_sSB4f0pj.webp](https://cdn.dong4j.site/source/image/20241229154732_sSB4f0pj.webp)
### 强迫症可能要急死
看见黄色警告了吗? 知道怎么改吗?
![20241229154732_2EOmbK8I.webp](https://cdn.dong4j.site/source/image/20241229154732_2EOmbK8I.webp)
![20241229154732_Yl0GTxks.webp](https://cdn.dong4j.site/source/image/20241229154732_Yl0GTxks.webp)
### 你就告诉我需要多宽的显示器?
老铁, 公司没给配这么宽的显示器啊... 啊, 27 寸的也看不过来啊
**超过 120 列宽必须需要换行**
![20241229154732_Mi5SqZ8G.webp](https://cdn.dong4j.site/source/image/20241229154732_Mi5SqZ8G.webp)
![20241229154732_d4IxIPHI.webp](https://cdn.dong4j.site/source/image/20241229154732_d4IxIPHI.webp)
**超过 5 个参数, 推荐使用实体类**
### 你还是去写 python 吧
![20241229154732_D7RhYdaO.webp](https://cdn.dong4j.site/source/image/20241229154732_D7RhYdaO.webp)
### 知道什么叫 util 吗?
![20241229154732_Q99tRBQJ.webp](https://cdn.dong4j.site/source/image/20241229154732_Q99tRBQJ.webp)
### Intellij IDEA 都知道会有空指针, 你还这么写?
![20241229154732_uckFF7xE.webp](https://cdn.dong4j.site/source/image/20241229154732_uckFF7xE.webp)
### 不能直接 return 吗? 练打字吗?
![20241229154732_s6WPNs6L.webp](https://cdn.dong4j.site/source/image/20241229154732_s6WPNs6L.webp)
### 面试题之 String, StringBuilder, StringBuffer
> JDK 5 以后 JVM 对字符串循环拼接的处理方式
![20241229154732_RDocgeHo.webp](https://cdn.dong4j.site/source/image/20241229154732_RDocgeHo.webp)
### 老铁, 类注释, 方法注释呢
**类注释呢?**
**方法注释虽然有, 但是不标准啊, 老铁**
没看见那么多黄色警告吗?
![20241229154732_nuQzSQA9.webp](https://cdn.dong4j.site/source/image/20241229154732_nuQzSQA9.webp)
**代码规范我们后面说**
![20241229154732_9wGwkuIH.webp](https://cdn.dong4j.site/source/image/20241229154732_9wGwkuIH.webp)
每个模块都有一个 StringUtil, 还有叫 StringUtils 的
老铁, 写之前先看看能不能复用啊, 或者复制之前, 看是不是已经有了啊.
### 你以为把 DDL 语句拷贝过来就不用写字段注释了吗?
老铁, 你这样骚操作我很为难啊
![20241229154732_1R7jMR50.webp](https://cdn.dong4j.site/source/image/20241229154732_1R7jMR50.webp)
在类上按 F1 看不到类注释啊
![20241229154732_C6AqFxhb.webp](https://cdn.dong4j.site/source/image/20241229154732_C6AqFxhb.webp)
这样改啊
![20241229154732_524c0JBV.webp](https://cdn.dong4j.site/source/image/20241229154732_524c0JBV.webp)
F1 直接看类注释啊, 不用跳转了啊
![20241229154732_H2KhddkH.webp](https://cdn.dong4j.site/source/image/20241229154732_H2KhddkH.webp)
F1 直接看字段注释啊, 不用再去查 DDL 了啊, 不会在蒙圈了啊
![20241229154732_guzkIspg.webp](https://cdn.dong4j.site/source/image/20241229154732_guzkIspg.webp)
### 老铁, 不是中文看不懂啊
额, 这个要怪 idea 了, 居然没有默认转换
![20241229154732_QtY0zZE5.webp](https://cdn.dong4j.site/source/image/20241229154732_QtY0zZE5.webp)
![20241229154732_mByeIzng.webp](https://cdn.dong4j.site/source/image/20241229154732_mByeIzng.webp)
老铁, 把 transpartent 打开, 你就认识中文了
![20241229154732_IvrhpyQY.webp](https://cdn.dong4j.site/source/image/20241229154732_IvrhpyQY.webp)
老铁, 看见黄色警告了? 如果是自己解析配置, 没有处理空白符的话, 又出 bug 了啊.. 啊.
### 老铁, 代码用 UTF-8 啊, 不然要乱码啊
![20241229154732_iqEm4YvC.webp](https://cdn.dong4j.site/source/image/20241229154732_iqEm4YvC.webp)
全都要 UTF-8 啊, 要跟国际接轨啊
![20241229154732_IY0UEuKK.webp](https://cdn.dong4j.site/source/image/20241229154732_IY0UEuKK.webp)
### 老铁, 0 是啥, 1 是啥, 2 又是啥啊? 脑壳都大了啊...
定义个常量啊, 常量名用拼音也比没有好啊, 老铁
![20241229154732_5g7sLkNB.webp](https://cdn.dong4j.site/source/image/20241229154732_5g7sLkNB.webp)
![20241229154732_isLnyliN.webp](https://cdn.dong4j.site/source/image/20241229154732_isLnyliN.webp)
### 论 MVC 架构的职责
dao 就是对表的操作, 一个 dao 对应一张表;
service 组合多个 dao 进行业务处理;
controller 做参数检查, 结果封装, 跳转页面;
![20241229154732_MUIpQN0h.webp](https://cdn.dong4j.site/source/image/20241229154732_MUIpQN0h.webp)
### 你咋不把所有的 sql 都写在一个 xml 里面呢?
![20241229154732_e35X2mlx.webp](https://cdn.dong4j.site/source/image/20241229154732_e35X2mlx.webp)
### 这个也要注入? 也能注入?
😅😂🤣
![20241229154732_CpUOdFER.webp](https://cdn.dong4j.site/source/image/20241229154732_CpUOdFER.webp)
### 多余的 finally
> redis-proxy 已经对 jedis 资源的安全释放做了处理, 不用自己在写这些冗余的代码
![20241229154732_0ggLpsRN.webp](https://cdn.dong4j.site/source/image/20241229154732_0ggLpsRN.webp)
### catch 里面不要做流程控制, OK?
![20241229154732_mUgmSDPp.webp](https://cdn.dong4j.site/source/image/20241229154732_mUgmSDPp.webp)
改为
![20241229154732_KxMkMlP9.webp](https://cdn.dong4j.site/source/image/20241229154732_KxMkMlP9.webp)
### log 输出错误
> 日志的正确使用姿势, 你值得了解一下
推荐去搜一下 log 的正确输出方式.
![20241229154732_HE2fZKMw.webp](https://cdn.dong4j.site/source/image/20241229154732_HE2fZKMw.webp)
```java
log.error("访问 redis 异常", e);
```
### 做人能不能真诚一点, 写代码能不能简单一点
![20241229154732_VwXgkaNB.webp](https://cdn.dong4j.site/source/image/20241229154732_VwXgkaNB.webp)
改为:
```java
IavpResponse iavpResponse = HttpUtil.sendPost(inputParams, "gatherkey", Integer.parseInt(timeOut));
if(iavpResponse.getStatusCode() == HttpStatus.SC_OK){
return XmlConverUtil.readGatherKeyXmlOut(iavpResponse.getContent());
}
```
XmlConverUtil.java
```java
public static List readGatherKeyXmlOut(String xml) {
if(StringUtils.isBlank(xml)){
return null;
}
...
}
```
### isNotEmpty 和 isNotBlank 的区别知道吗?
![20241229154732_3A8gDJsY.webp](https://cdn.dong4j.site/source/image/20241229154732_3A8gDJsY.webp)
改为:
![20241229154732_Aub2kvRm.webp](https://cdn.dong4j.site/source/image/20241229154732_Aub2kvRm.webp)
### 3 行代码搞定的事, 非要写几十行, 练打字吗?
**原始代码**
用于检查是否是会员
```java
public boolean judgeMiguSuperVIP(String caller) {
boolean VIPReturn = false;
// isMiguGameMothMember 表示游戏会员状态,1 表示是包月会员,0 表示不是包月会员
String isMiguGameMothMember = "0";
try {
GameAccount gameAccount = miguGameProvider.queryUserInfo(caller);
if (gameAccount != null) {
isMiguGameMothMember = gameAccount.getMiguSupperMember();
} else {
isMiguGameMothMember = "0";
}
if ("1".equals(isMiguGameMothMember)) {
// 是咪咕超级会员
VIPReturn = true;
return VIPReturn;
} else {
// 不是咪咕超级会员
VIPReturn = false;
return VIPReturn;
}
} catch (Exception e) {
// 查询游戏账号状态异常
VIPReturn = false;
return VIPReturn;
}
}
```
**重构 1**
删除 boolean VIPReturn
```java
public boolean judgeMiguSuperVIP(String caller, String type) {
// isMiguGameMothMember 表示游戏会员状态,1 表示是包月会员,0 表示不是包月会员
String isMiguGameMothMember = "0";
try {
GameAccount gameAccount = miguGameProvider.queryUserInfo(caller, type);
if (gameAccount != null) {
isMiguGameMothMember = gameAccount.getMiguSupperMember();
} else {
isMiguGameMothMember = "0";
}
return "1".equals(isMiguGameMothMember);
} catch (Exception e) {
return false;
}
}
```
**重构 2**
删除 isMiguGameMothMember
```java
public boolean judgeMiguSuperVIP(String caller, String type) {
// isMiguGameMothMember 表示游戏会员状态,1 表示是包月会员,0 表示不是包月会员
try {
GameAccount gameAccount = miguGameProvider.queryUserInfo(caller, type);
return gameAccount != null && "1".equals(gameAccount.getMiguSupperMember());
} catch (Exception e) {
return false;
}
}
```
**重构 3**
queryUserInfo 已经处理的下层抛出的异常, 这里不需要再处理
```java
public boolean judgeMiguSuperVIP(String caller, String type) {
// isMiguGameMothMember 表示游戏会员状态,1 表示是包月会员,0 表示不是包月会员
GameAccount gameAccount = miguGameProvider.queryUserInfo(caller, type);
return gameAccount != null && "1".equals(gameAccount.getMiguSupperMember());
}
```
## 日志问题
![20241229154732_TUqT6zHK.webp](https://cdn.dong4j.site/source/image/20241229154732_TUqT6zHK.webp)
### 老铁, 日志输出到文件要用 UTF-8 啊
不然乱码看不懂啊
![20241229154732_7KCouGBj.webp](https://cdn.dong4j.site/source/image/20241229154732_7KCouGBj.webp)
### 老铁, 日志输出能不能统一格式啊?
### 老铁, 日志输出能不能分级别啊?
### 老铁, 日志框架能不能统一使用一个啊?
一会 log4j, 一会 log4j2 的
做人喜新厌旧可以 (log4j2 更新, 效率更好)
但也要专一, 说好放学别走就不能走, 要跑...
说好用 log4j2 + slf4j, 就不要用 System.out.println() OK?
## Maven 问题
### 老铁, 一个模块这么多版本啊, 怎么管理啊
maven 用来管理项目中的依赖关系的, 这个没使用 maven 有什么区别?
![20241229154732_1JS1jb0y.webp](https://cdn.dong4j.site/source/image/20241229154732_1JS1jb0y.webp)
### 拷贝依赖的时候看没看是不是存在了?
![20241229154732_uvcka7Fo.webp](https://cdn.dong4j.site/source/image/20241229154732_uvcka7Fo.webp)
### 老铁, 不要只晓得拷贝依赖, 不看看依赖冲突啊
![20241229154732_T2zcOs2y.webp](https://cdn.dong4j.site/source/image/20241229154732_T2zcOs2y.webp)
## 项目结构问题
### 1000 个人眼里, 有 1000 个哈姆雷特, 1000 个人写代码, 就有 1000 种代码风格
![20241229154732_Mz06nD0Q.webp](https://cdn.dong4j.site/source/image/20241229154732_Mz06nD0Q.webp)
## 下一篇 musearch-project 介绍
已经重构的模块
![20241229154732_6WMOlgqE.webp](https://cdn.dong4j.site/source/image/20241229154732_6WMOlgqE.webp)
```lua
.
├── musicsearch-business # 业务主模块
│ ├── business-common # 业务公共类库
│ ├── mservice-migu-game # migu-game 业务
│ └── service-meeting # meeting 服务模块
├── musicsearch-common # musicsearch 项目 公共模块
├── musicsearch-component # 组件主模块
│ ├── component-iavp # iavp 模块, 封装 iavp 相关实体和接口, 直接注入即可
│ ├── component-mybatis # mybatis 模块, 提供代码生成和 mybatis 相关功能
│ ├── component-redis # redis 模块, 注入 RedisService 即可, 提供多种模式
│ └── component-websocket # websocket 模块 netty-socket.io 封装
├── musicsearch-demo # demo 主模块
│ ├── component-mybatis-demo
│ └── component-redis-demo
├── musicsearch-dependencies-bom # 管理第三方 jar 版本和依赖关系
├── musicsearch-monitor # 暂定
├── musicsearch-parent # musicsearch 工程主模块, 管理整个功能的版本及依赖
│ └── docs # 放工程相关文档
└── musicsearch-support # 支撑模块主模块
├── musicsearch-code-generator
├── musicsearch-management-system
└── musicsearch-timer-task
```
## [打造高效团队:统一编码风格的重要性](https://blog.dong4j.site/posts/a29c1a98.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
项目重构整理
## 废话
此篇是 12530 架构重构第二篇, 以 [阿里巴巴开发手册](https://github.com/alibaba/p3c/) 为基础, 结合自己工作经验, 作为 `musicsearch-project`
重构方案的基础部分.
以此为约束, 希望构建一个 `稳定`, `易于维护`, `可扩展` 的重构方案.
一个项目最怕多种编码风格, 实体名一会 entity, 一会 model, 让维护的人身心疲惫, 因此在一个项目中保持唯一一种编码习惯, 有利于代码维护 (比如通过命名,
就知道作用以及所在的包名).
**见名知意, 此乃命名的最高境界 (体会一下 `取名 10 分钟, 编码 1 分钟` 的境界 😂).**
> 代码规范看起来比较枯燥, 看完一遍可能只有一点点印象, 因此我采用比较逗比的方式, 尽量让大家一遍就记住.
> 代码规范比较偏向个人主义, 每个人的编码习惯都不一样, 所以希望多提出自己的建议, 一起改进 (没有最好的规范, 只有最合适的. 🙃)
## Let's go
### 开篇
话说盘古开天辟地之时... 亚当和夏娃诞生混沌之间, 他们从小青梅竹马, 一个会唱 200 首歌, 一个会跳 200 支舞, 后人称他们一个为 二百歌, 一个为
二百舞 ..... (用心去体会 😂)
### 这个才是开篇
江湖四分五裂, 一人一江湖...
但我们是一个团队,为了便于管理和维护,急需一份代码规范, 来约束我们的编码规则和习惯, 就像修炼一门武林绝学, 需要秘籍的指引 (葵花宝典?)。
请各位英雄好汉仔细阅读 严格遵守
如有不足之处 希望提出意见 共商武林统一霸业
从 1000 个哈姆雷特转变为同一个 **林志玲**, 代码风格保持统一有利于提高工作效率, 便于管理.
比如:
1. 一看到以 Controller 结尾的类, 就应该知道这个是接口, 用于参数验证, 调用业务类进行处理业务逻辑, 组装结果, 返回数据或者跳转页面;
2. 一看到以 Service 结尾的类, 就知道这个是业务接口类, 用于定义业务接口;
3. 一看到以 Impl 结尾的类, 就应该知道这个是业务实现类, 组合不同的 Dao, 实现业务逻辑;
4. 一看到以 Dao 结尾的类, 就应该知道这个是 DB 操作类, 对数据进行 CRUD 操作;
5. 一看到以 Dto 结尾的类, 就应该知道这个是数据传输对象, 用于展示层和服务层之间的数据传输;
6. ....
## 约定
为了避免歧义, 文档大量使用以下词汇, 解释如下:
1. `必须` (must): 绝对,严格遵循,请照做,无条件遵守;
2. `一定不可` (must not): 禁令,严令禁止;
3. `应该` (should): 强烈建议这样做,但是不强求;
4. `不该` (should not): 强烈不建议这样做,但是不强求;
5. `可以` (may) 和 可选 (optional): 选择性高一点,在这个文档内,此词语使用较少;
6. `推荐` (recommend): 个人推荐的做法, 不强求;
## 准备工作
> 行走江湖, 没有一件趁手的兵器怎么能在江湖中立足, 还在用 `eclipse` 的少侠们, 希望弃暗投明, 拥抱 Intellij IDEA, 你会发现编码效率提升不是
> **点巴点儿**, 而是 **蹭蹭蹭** 的往上涨 😁.
**我不是富二代, 我没有赢在人生的起跑线上, 但是我用 Intellij IDEA, 我赢在了工作的起跑线上!**
推荐一个比较全面的 [Intellij IDEA 教程](https://github.com/judasn/IntelliJ-IDEA-Tutorial)
**工欲善其事, 必先利其器. 一件趁手的兵器带你迎娶白富美, 走上人生巅峰, 统一江湖, 指日可待**
![20241229154732_uHJneu7y.webp](https://cdn.dong4j.site_uHJneu7y.webp)
为了简化手动操作和提高编码效率, 要求安装几个必要的插件, 以及对 IDEA 进行必要的优化配置
### Intellij IDEA 插件
#### Alibaba Java Coding Guidelines
> `必须` 安装, 代码规范检查的基础
**作用: 代码规则检查**
此插件是 阿里巴巴根据 [阿里巴巴开发手册](https://github.com/alibaba/p3c/) 开发的一个静态代码规范检查插件, 每个警告都提供了一个 demo,
![20241229154732_Hs7lxRAM.webp](https://cdn.dong4j.site_Hs7lxRAM.webp)
此篇以 [阿里巴巴开发手册](https://github.com/alibaba/p3c/) 为基础, 但是并不会一一罗列每条规范, 因为此插件已经能很好的检查了.
**此篇重点在于这个插件不能检查的规范.**
#### lombok
> `必须` 安装, 不然代码跑不起来就尴尬了
**作用: 化繁为简. [官网](<[http://projectlombok.org/](http://projectlombok.org/)>)**
使用 `@Data` 代替烦人的 get/set 方法
![20241229154732_0RVNjOoe.webp](https://cdn.dong4j.site_0RVNjOoe.webp)
使用 `@Slf4j` 代替获取 log 实例的代码
```java
private static final Logger log = LoggerFactory.getLogger(ApplicationTest.class);
```
![20241229154732_FeOsPGD7.webp](https://cdn.dong4j.site_FeOsPGD7.webp)
**使用方式**:
1. 在 pom 文件中添加:
```xml
org.projectlombok
lombok
${lombok.version}
```
2. 在 IDEA 中添加插件 `lombok` (file->setting->plugins)
3. IDEA 设置
![20241229154732_izw0KAnS.webp](https://cdn.dong4j.site_izw0KAnS.webp)
#### Maven Helper
> `必须` 安装, 不然依赖冲突了就不好了
**作用: 检查依赖冲突**
新加入一个 jar 包, 谁添加谁负责, 使用此插件检查是否有依赖冲突.
依赖冲突可能会导致:
1. java.lang.ClassNotFoundException
2. java.lang.NoSuchMethodError
3. java.lang.NoClassDefFoundError
4. 开发环境正常, 测试或者生产环境不正常....
![20241229154732_TxMWYfwd.webp](https://cdn.dong4j.site_TxMWYfwd.webp)
#### JavaDoc
> `必须` 安装
**作用: 快速生成标准的 javadoc**
![1111.gif](https://cdn.dong4j.site
#### JRebel
> `推荐` 安装, 提高工作效率的插件.
**作用: 代码热部署插件, 改了代码后, 重新编译, 不用重启应用就可查看效果.**
热部署插件, 谁用谁知道;
[科学使用方法](http://blog.lanyus.com/search/JRebel/) (低调点)
#### Mybatis Plugin
> `推荐` 安装, 提高工作效率的插件.
**作用: xml 和 dao 快速跳转; 快速生成 xml; xml 检查**
![222.gif](https://cdn.dong4j.site
#### GenerateSerialVersionUID
> `推荐` 安装, 提高工作效率的插件
**作用: 为实现了 Serializable 接口的实体快速添加 serialVersionUID, 提高效率**
因为要求实体 `必须` 实现 `Serializable` 接口, 而且 `必须` 添加 `serialVersionUID` 字段.
![333.gif](https://cdn.dong4j.site
#### GenerateAllSetter
> `推荐` 安装, 提高工作效率的插件
**作用: 快速生成 set 方法**
![444.gif](https://cdn.dong4j.site
#### Translation
> `推荐` 安装, 提高工作效率的插件
**作用: 翻译插件, 提供 百度, 有道, Google 翻译**
一款为像我这种英语渣的码农量身定做的插件 😂
![555.gif](https://cdn.dong4j.site
#### Grep Console
> `推荐` 安装
**作用: 高亮显示 log 不同级别日志,看日志的时候一目了然; 具有 error 级别声音提醒功能 (可设置)**
效果:
![20241229154732_NW1Z8dTj.webp](https://cdn.dong4j.site_NW1Z8dTj.webp)
插件设置:
![20241229154732_CExDSzg1.webp](https://cdn.dong4j.site_CExDSzg1.webp)
此插件通过 log 输出中的 info/debug/warn/error 来匹配对应的颜色. 因此 log 输出中必须包含 **日志级别**, 这个 **日志规范** 中再说.
#### RestfulToolkit
> `推荐` 安装
**作用: 快速定位接口, Rest 请求模拟 **
![666.gif](https://cdn.dong4j.site
#### Restore Sql for iBatis/Mybatis
> `推荐` 安装
**作用: 查看请求 sql, 可直接运行 **
效果:
`meeting-service` 调用 `/login` 接口后需要执行的 sql
![20241229154732_dUkaaD0g.webp](https://cdn.dong4j.site_dUkaaD0g.webp)
### Intellij IDEA 设置
#### 编码设置
> 编码 `必须` 使用 `UTF-8`, 且 `必须` 设置为 `with NO BOM`
![20241229154732_cNmFzTvf.webp](https://cdn.dong4j.site_cNmFzTvf.webp)
`踩坑`
**Windows 下请不要用记事本打开 UFT-8 编码的文本文件, 更不要保存 **
Windows 坑的很, UTF-8 编码的文本文件最前面会给你加上一个 BOM, 其他系统打开是正常显示, 但是不会显示这个 BOM, 造成文件解析出错.
##### html 设置编码为 UTF-8
```html
```
> html 或者模板 `应该` 使用 html5
html4 升级为 html5 非常简单
```html
Document
```
修改为:
```html
Document
```
简单的升级, 就能享受 30 多个新标签带来的便捷, 何乐而不为呢?
就跟超市打折一样, 还倒送你 30 块, 跳广场舞的大妈排着队去, 你还不去?
##### Tomcat 设置编码为 UTF-8
```xml
```
改为:
```xml
```
Tomcat7 默认编码为 ISO-8859-1, 到了 Tomcat8 后, 默认编码改为 UTF-8
因此以上修改只针对于 Tomcat7.
#### todo 标识
> `必须`
**作用: 方便搜索, 明确负责人, 说明 todo 原因 **
这里扩展了 IDEA 自带的 todo 标识, 使用 `todo- 负责人: (时间) [原因]` 来规范 `todo` 用法
![20241229154732_2ZRdEKBJ.webp](https://cdn.dong4j.site_2ZRdEKBJ.webp)
```lua
todo- 负责人: ($date$ $time$) [$SELECTION$]
```
**date time 设置见 类注释一节 **
效果:
![777.gif](https://cdn.dong4j.site
#### fixme 标识
> `必须`
```lua
fixme- 负责人: ($date$ $time$ [$SELECTION$])
```
设置和效果同上
#### 类注释
> `必须` 为每个类添加必要的注释
这里有 2 中方式:
**1. 新建类时添加注释 **
![888.gif](https://cdn.dong4j.site
设置方式:
![20241229154732_xjBq89Wf.webp](https://cdn.dong4j.site_xjBq89Wf.webp)
```java
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
##parse("File Header.java")
/**
* Company: xxxx公司
* Description: ${description}
¶*
* @author 你的昵称
* @date ${YEAR}-${MONTH}-${DAY} ${HOUR}:${MINUTE}
* @email 用户名@xxxx.com
*/
public class ${NAME} {
}
```
**2. 为已存在的类添加注释 **
![999.gif](https://cdn.dong4j.site
设置方式:
![20241229154732_FMVEAJ28.webp](https://cdn.dong4j.site_FMVEAJ28.webp)
```java
/**
* Company: xxxx公司
* Description: $END$
*
* @author 你的昵称
* @date $date$ $time$
* @email 用户名@xxxx.com
*/
```
#### 方法注释
> `推荐` 使用 JavaDoc 插件自动生成
JavaDoc 只能自动生成非 `private` 方法和属性的注释, 如果需要生成 `private` 级的注释, 可以通过修改 `public`, 生成完成后, 再修改回来
#### 代码注释
> `不该` 使用行尾注释;
> `推荐` 使用 // 代替 `/*...*/` 多行注释;
> `推荐` 使用 `/**..*/` 对字段进行注释 (单行);
> 所有字段名, 方法名, 类名 都 `应该` 使用简单, 业界常用的单词命名, 不要为了注释而注释, 讲究个代码之美.
在查看大段代码时, `推荐` 使用
```java
// region 段注释
....
// endregion
```
**此注释能增加代码折叠功能, 便于快速梳理代码 **
![11111.gif](https://cdn.dong4j.site
### Version Control 设置
> `必须` 忽略 target 目录
> `必须` 忽略 .idea
> `必须` 忽略 `*.iml`
> `必须` 添加 .ignore 文件
**如果使用 git, 所有忽略文件都可以添加到 .ignore 即可 **
**提交代码时的设置 **
![20241229154732_RjJbNzVS.webp](https://cdn.dong4j.site_RjJbNzVS.webp)
> `必须` 勾选 Optimize imports; (提交代码时, 自动删除不被使用的 import)
> `推荐` 取消掉默认勾选的 Perform code analysis 和 Check TODO, 会增加提交代码的时间;
> `不该` 勾选 Reformat code, 会格式化所有提交的代码, 格式化代码应该自己手动格式化, 这样能减少代码冲突的可能;
## 代码样式
以 intellij-java-google-style.xml 为基础, 做了一部分修改.
> musicsearch-project `必须的必须` 使用此代码样式, 为了减少代码冲突, 同一代码样式 (同一门派, 你去修炼其他门派武功, 会被逐出师门的!).
比如:
1. 一行的代码长度不能超过 140 宽;
2. 等号对齐;
3. if 语句只有一行也 `必须` 加大括号;
4. 所有操作符两边 `必须` 有空格;
5. 类名, 方法名 `必须` 加空格后才跟 一个 `{`;
6. 使用 4 个空格代替制表符 (我们这里不讨论空格好还是制表符好, 统一使用一种是最最好的);
7. ....
**以上列举的部分规范都可以使用格式化解决;**
本人对代码有着严 (变) 格(态)的要求, 严 (变) 格(态)到使用 `//` 后面 `必须` 跟一个空格, 然后才是注释内容; 英文和中文之间 `必须` 增加一个空格;
所有标点全部使用英文标点; 没有被使用的类, 变量都 `必须` 删除; 遇到的警告 `必须` 尽最大努力改正;
左边为重构之前的警告数量 (黄色), 右边为重构之后的
![20241229154732_b8RAQPH9.webp](https://cdn.dong4j.site_b8RAQPH9.webp)
1. 英文和中文之间增加空格是为了便于阅读, 方便拷贝 (双击英文即可全选, 如果不增加空格, 中文英文则会全被选中. 我变态到 chrome
都要安装一个叫 `空格之神` 的插件, 用于在中英文之间追加空格);
2. 全部使用英文标点的好处不言而喻, Java 乃至所有的编程语言都不支持中文标点, 打个分号还得切换输入法, 想想都忧伤; 有时候就是因为中文标点的问题,
一直报错 (输入法可以设置中文时使用英文标点, `推荐` 这种设置);
3. 符号后面喜欢追加一个空格, 是受 Markdown 语法的影响, 以前用过很多 Markdown 编辑工具, 由于解析语法不一样, 迁移时渲染不生效, 加个空格或者换行就
Ok 了; 😂
4. 及时删除不需要的代码, 时刻保持代码干净 (IDEA 会帮我们检查未使用的代码).
5. **警告是 bug 的温床, 修复警告, 就是修复潜在的 bug**. 修复警告, 也能学到很多东西的 (😎)
## musicsearch-project 规范
以下作为 `musicsearch-project` 规范, 制定了大部分要求.
具体设计将在 第三篇 介绍
### 项目结构规范
```lua
.
├── musicsearch-business # 业务主模块
│ ├── business-common # 业务公共类库
│ ├── mservice-migu-game # migu-game 业务
│ └── service-meeting # meeting 服务模块
├── musicsearch-common # musicsearch 项目 公共模块
├── musicsearch-component # 组件主模块
│ ├── component-iavp # iavp 模块, 封装 iavp 相关实体和接口, 直接注入即可
│ ├── component-mybatis # mybatis 模块, 提供代码生成和 mybatis 相关功能
│ ├── component-redis # redis 模块, 注入 RedisService 即可, 提供多种模式
│ └── component-websocket # websocket 模块 netty-socket.io 封装
├── musicsearch-demo # demo 主模块
│ ├── component-mybatis-demo
│ └── component-redis-demo
├── musicsearch-dependencies # 管理第三方 jar 版本和依赖关系
├── musicsearch-monitor # 监控主模块
├── musicsearch-parent # musicsearch 工程主模块, 管理整个功能的版本及依赖
│ └── docs # 放工程相关文档
│ └── database # 放工程 sql
└── musicsearch-support # 支撑模块主模块
├── musicsearch-code-generator
├── musicsearch-management-system
└── musicsearch-timer-task
```
目前重构后的模块, 可能还会有修改.
但是几个主要模块不会再修改了.
> 整个项目结构采用 `Maven` 多模块的方式开发.
> 目的是为了解决 jar 依赖混乱问题.
**结构规范:**
> 所有主模块 `必须` 以 `musicsearch-` 开始.
不为别的, 纯属统一前缀, 好看而已
> 模块名 `必须` 全部使用小写, 单词之间作用 `-` 分隔;
#### musicsearch-parent
作为整个项目的 灵魂 模块, 管理所有自有模块的版本和依赖关系, 以及整个项目都会使用到的依赖, 比如 `lombok`, `junit` 等.
![20241229154732_FNQjsndy.webp](https://cdn.dong4j.site_FNQjsndy.webp)
`dependencyManagement` 标签只是用于声明可能会被使用到的依赖 (就像定义变量), 不会真正添加依赖
`dependency` 才会真正真正引入依赖
这里全部为 `musicsearch-project` 自有模块, 以后新增的模块也 `必须` 将声明添加到此标签下
自有模块之间依赖就可直接使用.
![20241229154732_vXVvcVaj.webp](https://cdn.dong4j.site_vXVvcVaj.webp)
使用时, `一定不可` 添加 version 标签, 不然就会使用修改后的版本, 可能会造成依赖冲突.
---
`musicsearch-project` 模块下有一个 doc 目录, 用于保存文本文档
个人认为, 项目的 `技术文档` 跟着代码走才是最正确的做法.
随时修改, 随时查看, 代码和文档一起更新提交, 才是最佳实践.
![20241229154732_Sc2zPY5K.webp](https://cdn.dong4j.site_Sc2zPY5K.webp)
技术文档 `推荐` 使用 `Markdown` 语法编写, 最好是 [GitHub Markdown 语法](https://guides.github.com/features/mastering-markdown/).
这里给出模块说明模板, 每个模块都 `必须` 有此文档
```markdown
# 模块名
## 简介
xxx
## 打包方式
xxx
## 部署方式
xxx
## 使用说明
1. xxx
2. xxx
3. xxx
## 注意事项
xxx
## 更新历史
**更新时间 更新人 **
1. xxx
2. xxx
```
---
`database` 用于保存需求需要更新的 sql 文件, `必须` 以 `.sql` 为后缀, `必须` 是 UTF-8 编码.
#### musicsearch-dependencies
此模块是为了方便管理第三方 jar 依赖而特意添加的, 如果没有此模块, 第三方依赖也可以添加到 `musicsearch-project` 中, 但是会造成过度臃肿,
因此将第三方依赖拆分到此模块中进行统一管理.
> 第三方依赖 `必须` 添加到此模块, 且 `必须` 将版本号设置到 properties 标签下.
> 第三方依赖较大可能存在版本冲突, 因此此模块的版本从 `0.0.1` 开始, 不和 `musicsearch-parent` 相同, 如果此模块存在依赖问题, 修复后, 需要提升版本号,
> 并且 `必须` 写更新记录.
此模块比较特殊, 修改会比较频繁, 依赖出错大部分出现在此模块, 因此单独写一个 `changes.md`, 用于记录更新日志.
![20241229154732_mPHwivSa.webp](https://cdn.dong4j.site_mPHwivSa.webp)
**引入新依赖步骤 **
1. 在 `musicsearch-dependencies` 添加新依赖;
2. 将版本信息写入到 pom 的 properties 标签内, 进行统一管理;
3. 在需要的模块中, 引入依赖;
4. 最后使用 `Maven Helper` 排查是否存在依赖冲突;
5. 如果存在冲突 需要使用 `` 排除相应的依赖;
#### musicsearch-common
`musicsearch-project` 项目的子模块, 作为最底层的依赖, 提供了公共 util 包, core 包, base 包等基础代码.
> 如果被整个项目使用到的代码, 都 `必须` 放入此模块, 比如 StringUtils 工具类, 一些加密类, 整个项目都能使用到的常量类, 枚举类等;
**必须要提出的是:**
不要多个模块多个 StringUtils 工具类, 最佳实践 `应该` 是 `musicsearch-common` 中编写一个通用的 `StringUtils` 类,
继承自 `org.apache.commons.lang3.StringUtils` 类, 业务模块继承 `musicsearch-common` 模块中的 `StringUtils` 类,
实现业务相关的字符串处理工具类.(这种方式同样适用于其他类)
#### musicsearch-business
业务模块的父模块, `musicsearch-parent` 模块的子模块.
用于管理业务模块共用的 jar 依赖.
> 所有业务模块 `必须` 是 `musicsearch-business` 的子模块
将业务代码全部整合在一个模块中, 使用业务名进行再分子模块的方式, 管理整个业务代码.
其他共用模块作为框架基础模块, 以后新项目还可以复用.
> 不提供 `dubbo service` 的模块, `必须` 以 `service-` 开始.
> 提供 `dubbo service` 的模块, `必须` 以 `mservice-` 开始.
`mservice` 的意思是 `micro-service`. 这样便于区分不同的服务类型, 也方便分组.
我们现在使用 `dubbo` 服务治理框架, 如果以后有可能话, 可以很方便的迁移到 `SOFA` 或者 `Spring Cloud` (我就说说, 应该是不可能的事了).
业务子模块除了 `business-common` 模块, 其他子模块都以 Web 应用 或者 JVM 进程直接提供服务.
#### business-common
`musicsearch-business` 模块的子模块, 依赖于 `musicsearch-common`.
> 业务模块共用的代码 `必须` 放入 `business-common` 模块.
比如所有业务都会用到的 redis key 常量类, 则 `必须` 放在 `business-common` 模块中;
而 `service-meeting` 这个模块的业务用到的 redis key 常量则 `必须` 放到 `service-meeting` 模块中.
讲究职责分离, 层级分明, 互不干涉; 你走你的阳关道, 我看我的 西虹市首富.
### 模块名
> `必须` 全部为小写, 单词之间 `必须` 使用 `-` 分隔.
不要随心所欲的命名, 要讲究的规则, 不然会走火入魔的.
> provider 模块名 `必须` 以 `mservice-` 开头;
provider 模块也有可能是服务消费者, 这类的模块, 还是 `应该` 使用 `mservice-` 开头
对于 provider 模块, 至少应该分为 2 层;
1. 服务名 -interface , 提供给 consumer 的依赖模块
2. 服务名 -service , 业务处理模块
> 服务名 -interface 模块 `必须` 包含用到的实体类, 接口定义, dubbo-consumer- 服务名.xml, 共用的 util 类, 枚举类等;
**必须要提出的是:**
`dubbo-consumer- 服务名.xml` 配置文件 `应该` 由 provider 来维护, 而 consumer 只需要 import 此配置文件即可, 而不是在自己的 Spring
配置文件或者自定义一个 dubbo 配置文件再来写引入的接口.
这就是为什么 `dubbo-consumer- 服务名.xml` `应该` 在 interface 模块的原因.
这里看看 migu-game 重构之前的结构 (有一些小改动)
**配置关系:**
![20241229154732_Fx0TE6bY.webp](https://cdn.dong4j.site_Fx0TE6bY.webp)
这里的 dubbo.xml 即 provider 配置, 前面也说了, 这样的命名方式不够直观, 因此 `应该` 采用 `dubbo-provider- 服务名 ` 的方式命名;
Spring-config.xml 是主配置, 导入了其他 5 个配置;
```xml
```
`applicationContext-jms.xml` 和 `applicationContext-redis.xml` 这 2 个配置原来是放在 web.xml 中的.
这里迁入到 Spring-config.xml 中, 因为 web.xml 只需要负责加载 Spring-config.xml 即可, web.xml 写好之后, 基本不需要修改, 所有配置关系全部在
Spring-config.xml 中管理.
那么问题来了
**dubbo 的 consumer 配置在哪里呢?**
找了半天, 原来在 funclib 模块的 `applicationContext-mainflowfunc.xml` 配置中...
![20241229154732_F89Aye00.webp](https://cdn.dong4j.site_F89Aye00.webp)
那么问题又来了
如果 migu-game 由合肥的同事开发, 需要增加几个接口, 使用 migu-game 服务的是成都的小明同学, 正在开发 funclib, 这个时候请问....
**小明中午吃了啥?**
![20241229154732_YSkFqKZ8.webp](https://cdn.dong4j.site_YSkFqKZ8.webp)
> 如果 dubbo consumer 配置由 funclib 维护, 那么就要修改 funclib 模块的配置;
> 如果 dubbo consumer 配置由 migu-game 维护, 合肥的同事只需要提供 migu-game-interface, funclib 只需引入 `dubbo-consumer-migugame.xml`;
` 服务名 -interface` 还应该依赖 `dubbo` 相关 jar 依赖, 这样 consumer 就不需要自己引入 `dubbo` 相关依赖了;
服务行业嘛, 要做就做全套
**最佳实践 **
个人认为, 一个 `mservice` 最好分为 3 层;
1. interface 层;
2. service 层;
3. dao 层;
interface 已经说过了;
service 层, 作为业务层, 依赖于 dao 层和 interface 层;
各层的 pom 中只依赖每层需要的 jar 依赖. 比如 duubo 的依赖 `应该` 写在 interface 层, `mybatis` 和数据库驱动依赖 `应该` 写在 dao 层, 而
service 层依赖业务相关的 jar.
这样将所有依赖下放到不同层中, 以一种插件化的方式提供服务, 导入某层依赖, 连带引入了这层需要的依赖. 这样 jar 依赖关系明确, 也很好管理.
`musicsearch-project` 的思想就是这样, 以 `分而治之` 的方式管理代码和依赖关系, 也就是个解耦的思想.
而不是将所有依赖全部扔到 common 或者 service 中, 方便是方便, 维护起来想死的心都有了.
interface 模块, 就是一个提供给 consumer 的说明, 告诉 consumer, 我这个 interface 提供了哪些功能, 没有具体实现.
而 API 全称 Application Program Interface, 即应用程序接口, 是一组定义, 程序及协议的集合.
因此我将 服务名 -api 用于向外提供 rest api 的模块.
相关的包名也是如此.
### 包名
> 所有包名 `必须` 以 `com.xxxx.msearch` 开始
其他规则:
- 组件模块: `com.xxxx.msearch.component. 组件名 `
- 公共类库: `com.xxxx.msearch.common`
- 服务模块: `com.xxxx.msearch. 服务名 `
`推荐` 几个常用的包名
- `config` 用来放配置类
- `util` 工具类包
- `enums` 枚举类包
- `constant` 常量类包 (`推荐` 将常量按类别定义在不同的常量类中)
- `service` service 接口包
- `service.impl` service 接口实现类包
- `dao` dao 接口包
- `interfaces` 特指 provider 提供的接口类
- `api` 特指 rest api
### 类名
类名 `必须` 使用 UpperCamelCase 风格 (首字母都大写),必须遵从驼峰形式. 例如: StringUtils
> 抽象类命名 `必须` 使用 Abstract 或 Base 开头;
> 异常类命名 `必须` 使用 Exception 结尾;
> 测试类命名 `必须` 以它要测试的类的名称开始,以 Test 结尾;
> 接口实现类 `必须` 以 Impl 结尾;
> API 接口类必须以 `Controller` 结尾;
> ORM 接口必须以 `Dao` 结尾;
### 方法名
Dao 接口名 `推荐` 使用以下命名方式
新增:
- add(Entity entity)
- insert(Entity entity)
- save(Entity entity)
查询:
- get(Long id)
- getByXxx
- findByXxx
- selectByXxx
更新:
- update(Entity entity)
- updateByXxx
删除:
- delete(Long id)
- deleteByXxx
### 属性名
这个没什么好说的, 按照 Java 推荐命名方式即可.
> 常量命名 `必须` 全部大写, 单词间用下划线分隔;
## 数据库规范
推荐 3 篇文章, 虽然是 mysql 的, 但是都是数据库啊
[赶集 mysql 军规](https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651960775&idx=1&sn=1a9c9f4b94dfe71ad2528fb2c84f5ec7&chksm=bd2d001b8a5a890d302d139ea42e9ffde44407738a618865934e40b8e35486b13cafca2933f6&mpshare=1&scene=1&srcid=1228MzgFw9KLVzaHtjHvpb2p%23rd)
[58 到家数据库 30 条军规解读](https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959906&idx=1&sn=2cbdc66cfb5b53cf4327a1e0d18d9b4a&chksm=bd2d07be8a5a8ea86dc3c04eced3f411ee5ec207f73d317245e1fefea1628feb037ad71531bc&scene=21#wechat_redirect)
[再议数据库军规](https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959910&idx=1&sn=6b6853b70dbbe6d689a12a4a60b84d8b&chksm=bd2d07ba8a5a8eac6783bac951dba345d865d875538755fe665a5daaf142efe670e2c02b7c71&scene=21#wechat_redirect)
## 配置规范
为了解决开发, 测试, 现网部署时手动对比配置, 手动切换配置, 减少人工介入出错的几率, 节约时间等问题, `musicsearch-project` 采用多环境配置.
一次打包, 可部署到多个环境.
意思就是: 一次打包, 到处运行.
### 配置分类
**从数据来源可分为:**
- 数据库
- NoSQL
- 文件
- 网络
**从加载顺序上课分为:**
- 启动时加载的配置
- dataSource 连接配置
- Redis 连接配置
- MQ 连接配置
- 日志配置
- ...
- 运行期间动态获取
- 字典数据
其实最容易出错的且影响最大的就是运行期间动态获取的配置, 字典数据改错了就会影响现网业务, 而且不好排查.
连接配置启动时就需要, 如果错误可能会导致启动失败, 这类问题比较容易排查.
### local 环境
以前在开发时, 使用的 dev 环境. 可能会将某几个配置修改成本地环境或者 test 环境进行开发, 开发完成后, 又 `需要逐个改回来`;
改动的配置太多, 部分配置 `忘记改回来` 还提交了, 悲剧了;
local 环境专门解决以上问题.
最初是一个空配置, 我们拉取代码后根据自己的环境修改 local 配置, 然后使用 idea `忽略提交` 功能, 忽略此文件.
每个开发者互不影响, 开发时只需要修改 local 环境即可, `不该` 直接修改其他环境配置,避免环境问题造成线上问题
### dev 环境
现在的服务太多, 我们开发时不可能每个都启动起来, 这时我们可以将一些公共的服务部署到 dev 服务器, 这个配置就是 dev 环境的配置了,
一旦配置好, `不该` 轻易修改.
### test 环境
和 dev 环境差不多, 只是一些连接配置不一样, `不该` 轻易修改.
### prod 环境
生产环境经常改动的就是字典数据, 现在的字段数据太复杂了.
最好的方式是通过 kv 键值对, value 只是简单的字符串而已, 不要是一个复杂的 json, 这样能非常友好的管理所有配置, 修改配置也不容易出错.
redis 构建工具我了解当的一个作用是为了防止修改出错, 做了一个保护措施.
先保存到数据库中, 检查一下, 确保没出错后使用构建工具同步到 redis.
如果是这个原因的话, 我想能不能使用一个弹出框显示修改前与修改后的值, 然后二次确认?
确认后修改数据库, 同时更新 redis.
获取动态数据的方式:
首先从 redis 中获取, 如果没有则从数据库中获取, 然后更新到 redis;
如果有则直接返回.
后台系统修改字典数据且二次确认后, 更新数据库和 redis.
这里有一个事务问题.
比如后台系统修改完字段数据后, 更新数据库成功, 但是更新 redis 失败, 这个时候 redis 中就是旧数据.
**解决方案:**
1. 后台系统二次确认修改;
2. 更新数据库;
3. 将 redis 相应的 key 设置为失效;
**分析:**
1. 第 2 步失败, 直接返回修改失败, 不会发生事务问题, 数据也不会被修改;
2. 第 2 步成功, 第 3 步失败;
1. 此时应用查询字典数据时, redis 为旧数据 (可以使用重试来解决)
3. 第 2 步成功, 第 3 步成功;
1. 此时应用查询字典数据时, redis 没有, 则去数据库中查询最新, 然后再更新到 redis 中.
### Dubbo 配置文件
> 消费者配置文件名 `必须` 以 `dubbo-consumer- 服务名.xml` 命名.
> 服务提供者配置文件名 `必须` 以 `dubbo-provider- 服务名.xml` 命名.
配置名区分消费者和生产者, 这样职责明确, 方便搜索查看. 最操蛋的是搜索出几十个 dubbo.xml 配置, 还得一个个点进入看是消费者还是生产者配置.
> 消费者配置 `必须` 单独写在 `dubbo-consumer- 服务名.xml` 中, 不要写在 Spring 配置中.
配置尽量分开到不同到配置文件中, 最后使用 import 聚合起来. 需要修改配置时, 能明确知道去哪个配置文件中修改.
你见 `葵花宝典` 里面写了怎么自宫的详细教程吗? 最多也就引用一下, `欲练此功, 必先自宫`.
> 引入配置不要写在 web.xml, `必须` 使用 import 在 Spring 主配置文件中引入.
`musicsearch-project` 不需要担心这个问题, 因为 web.xml 已经被干掉了 🤣
### Spring 配置文件
- SpringMVC 配置文件统一命名: `spring-mvc.xml`
- Web 应用的 Spring 配置文件统一命名: `spring-context.xml`
- 其他组件的 Spring 配置文件统一命名: 模块名.xml
- Spring Boot 应用的配置文件统一使用 application.properties
`musicsearch-project` 使用 Spring Boot 为基础框架, 因此 `spring-mvc.xml` 和 `spring-context.xml` 已经使用 Java Config 的方式代替.
**自动配置类 `必须` 以 服务名 +Configuration 方式命名 **
**启动类 `必须` 以 服务名 +Application 方式命名 **
**单元测试主类 `必须` 以 服务名 +ApplicationTest 方式命名 **
## Maven
原来的项目是由多个模块由不同的 Maven 管理, 造成编译, 打包,debug 麻烦, 依赖关系复杂, 版本随意, 同一个模块多个不同版本, 维护困难.
为了解决以上问题, 现在将所有模块通过一个 主 pom 进行管理, 所有模块全部在 `musicsearch-porject` 目录下.
优点如下:
- debug 时方便, 想在哪儿打断点就在哪儿打断点. 随处 debug;
- 能借助 IDEA 进行全局重构;
- jar 版本统一管理
- 绝对不会出现相同 jar 多个相同版本的问题.
### pom 规范
> groupId `必须` 为 `com.xxx`
> artifactId `必须` 与 模块名相同
> version `必须` 与 父 pom version 相同
> `必须` 显式指定 `packaging`
如果没有设置 packaging 标签, 默认打包为 jar 格式, 这里 `必须` 设置此标签, 明确指定打包格式.
> `必须` 写 `description` 标签
为此 pom 添加必要的描述信息
> 第三方依赖 `必须` 添加到 `musicsearch-dependencies` pom 中;
> 如果 Maven 中央仓库没有的 jar 包, 从网上下载后, `必须` 上传到公司的 Maven, 不能直接使用 jar 包
1. 搜索 Maven 中央仓库, 关键字 + maven;
2. 搜索不到则找到 jar 上传到公司 Maven 仓库
```
# 安装到私服
# DgroupId 和 DartifactId 构成了该 jar 包在 pom.xml 的坐标,项目就是依靠这两个属性定位。自己起名字也行。
# Dfile 表示需要上传的 jar 包的绝对路径。
# Durl 私服上仓库的位置,打开 nexus——>repositories 菜单,可以看到该路径。
# DrepositoryId 服务器的表示 id,在 nexus 的 configuration 可以看到。 要与 setting.xml 中的权限 id 一致
# Dversion 表示版本信息
mvn deploy:deploy-file -DgroupId=com.xxx -DartifactId=yyy -Dversion=x.x.x -Dpackaging=jar -Dfile=jar-path -Durl= 上传地址 -DrepositoryId=thirdparty
```
> 第三方依赖 `必须` 将版本迁入 `musicsearch-dependencies` 到 properties 标签下;
> version `必须` 以 `artifactId.version` 的方式命名;
```xml
...
1.0.0
...
io.socket
socket.io-client
${socket.io-client.version}
```
由于 `musicsearch-project` 使用 Spring Boot 开发, 版本为 1.5.8.RELEASE.
Spring Boot 每个版本都有一个 `spring-boot-dependencies` 项目用来维护所有第三方 jar 版本和 Maven 插件版本.
![20241229154732_HijijtDI.webp](https://cdn.dong4j.site_HijijtDI.webp)
我们直接使用 `spring-boot-dependencies` 中相关依赖的版本, 能有效减少版本冲突.
> 如果我们新增的第三方包已经在 `spring-boot-dependencies` 声明, 则 `必须` 使用 `spring-boot-dependencies` 规定的版本, 即删除 version 标签.
怎么查看新引入的依赖是否在 `spring-boot-dependencies` 被声明了呢, 很简单
比如我需要添加如下依赖到模块中
```xml
org.projectlombok
lombok
1.16.14
```
整个操作如下图所示:
![2222.gif](https://cdn.dong4j.site
我们新增的 lombok 版本为 1.16.14
但是当加入 pom 之后, 左边出现了一个向上的箭头, 点过去之后, 发现此依赖已经在 `spring-boot-dependencies` 被声明了, 并且版本为 1.16.18, 因此删除
version 就可以了.
**添加新依赖的步骤:**
1. 选择合适的模块, 直接粘贴 maven 依赖配置到 pom 中,
2. 如果左边出现了向上的箭头, 则删除 version 标签, 搞定;
3. 如果没有, 则将此依赖配置粘贴到 `musicsearch-dependencies` 中, 将 version 添加到 properties 标签中;
4. 最后删除最开始那个配置的 version;
`spring-boot-dependencies` 不可能定义到所有的依赖, 因此这里就有了 `musicsearch-dependencies` 这个模块,
用来管理项目需要但是没有在 `spring-boot-dependencies` 中定义的 jar 依赖.
## 日志规范
> `推荐` 使用 `@Slf4j` 获取 `log` 实例
使用 `lombok` 插件, 直接使用 @Slf4j 代替获取 log 实例的冗余代码.
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
```
> `必须` 使用 `log4j2` 日志框架
统一使用效率更高的 log4j2 日志框架
> 日志输出到文件的编码 `必须` 为 UTF-8
### 日志配置名
> Web 应用或者通过 JVM 进程提供服务的应用, `必须` 使用 `log4j2-spring.xml` 命名
Spring Boot 官方推荐优先使用带有 -spring 的文件名作为日志配置(如使用 log4j2-spring.xml,而不是 log4j2.xml)
原理可以查看
`Log4J2LoggingSystem.getCurrentlySupportedConfigLocations()`
`AbstractLoggingSystem.getSpringConfigLocations()`
### 输出样式
```
env: %d{yyyy.MM.dd HH:mm:ss.SSS} [%5p] ${sys:PID} -- [%15.15t] %-40.40c{1.} : %m%n%xwEx
```
输出内容元素具体如下:
- 环境名
- 时间日期 — 精确到毫秒
- 日志级别 — ERROR, WARN, INFO, DEBUG or TRACE
- 进程 ID
- 分隔符 —`--`标识实际日志的开始
- 线程名 — 方括号括起来(可能会截断控制台输出)
- Logger 名 — 通常使用源代码的类名
- 日志内容
效果 :
```
dev: 2018.08.01 15:02:25.153 INFO 75012 -- [restartedMain] c.i.m.m.MeetingApplication : Starting MeetingApplication on dong4j with PID 75012 (/Users/codeai/Develop/work/ifly/musicsearch-project/musicsearch-business/service-meeting/target/classes started by codeai in /Users/codeai/Develop/work/ifly/musicsearch-project)
dev: 2018.08.01 15:02:25.154 DEBUG 75012 -- [restartedMain] c.i.m.m.MeetingApplication : Running with Spring Boot v1.5.8.RELEASE, Spring v4.3.12.RELEASE
dev: 2018.08.01 15:02:25.155 INFO 75012 -- [restartedMain] c.i.m.m.MeetingApplication : The following profiles are active: oracle
```
### 日志保存路径
日志路径统一管理, 确保每台服务器上的日志都在同一目录下
```
/path/to/logs/${APP_NAME}
```
`/path/to/logs/` 再议, 只要是一个有读写权限的目录且好记就可以了, 关键是保持统一.
> 最后 `必须` 通过 APP_NAME 参数区分应用.
### 日志归档
> 日志文件 `必须` 1 天归档一次,压缩文件上限 `建议` 为 200MB
每天归档日志, 方便按日志查询日志
## 单元测试规范
> 所有模块都 `必须` 有单元测试, 而不是使用 `main()` 来进行测试;
IDEA 添加单元测试类非常简单
![3333.gif](https://cdn.dong4j.site
直接通过快捷键自动生成单元测试类.
> 集成测试时, `必须` 继承主测试类;
在每个 Web 模块中, 都会有一个 XxxApplicationTest 测试父类, 用于整合配置类, 测试端口随机等功能.
![20241229154732_JacCQkxj.webp](https://cdn.dong4j.site_JacCQkxj.webp)
其他集成测试类只需要继承此父类即可, 不需要写重复的注解
![20241229154732_8Sa4oiU5.webp](https://cdn.dong4j.site_8Sa4oiU5.webp)
默认是单元测试 `必须` 全部通过才能打包, 当往往由于单元测试编写不规范造成打包失败, 编写好的单元测试难度也非常大, 因此这里不强制要求.
使用 3 种方式忽略单元测试
**方式 1:**
`推荐` 这种
使用变量
```xml
true
```
或者
```xml
true
```
**方式 2:**
使用 mvn 命令
```lua
mvn package -Dmaven.test.skip=true
```
**方式 3:**
使用插件
```xml
org.apache.maven.plugins
maven-surefire-plugin
${maven-surefire-plugin.version}
true
```
方式 1 与方式 2 的区别在于:
`skipTests` 不执行测试用例,但编译测试用例类生成相应的 class 文件至 target/test-classes 下
`maven.test.skip` 不但跳过单元测试的运行,也跳过测试代码的编译
## [Redis Sentinel 搭建指南:从入门到精通](https://blog.dong4j.site/posts/70b9e46a.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
记录 Redis Sentinel 的搭建过程
## 现有架构的问题
1. master 挂掉之后, 需要手动切换, 运维复杂
## Redis Sentinel 高可用
![20241229154732_8Zk7xKho.webp](https://cdn.dong4j.site/source/image/20241229154732_8Zk7xKho.webp)
下面以 1 个主节点、2 个从节点、3 个 Sentinel 节点组成的 Redis Sentinel 为例子
故障转移处理逻辑:
1. 主节点出现故障, 此时两个从节点与主节点时区连接, 主从复制失败;
![20241229154732_HwvXcbfZ.webp](https://cdn.dong4j.site/source/image/20241229154732_HwvXcbfZ.webp)
2. 每个 Sentinel 节点通过定期监控发现主节点出现故障;
![20241229154732_aQBkbupn.webp](https://cdn.dong4j.site/source/image/20241229154732_aQBkbupn.webp)
3. 多个 Sentinel 节点对主节点的故障达成一致, 选举出 sentinel-3 节点作为领导者负责故障转移;
![20241229154732_s7YZQex5.webp](https://cdn.dong4j.site/source/image/20241229154732_s7YZQex5.webp)
1. 原来的从节点 slave-1 称为新的主节点后, 更新应用方的主节点信息, 重新启动应用方;
2. 客户端命令另一个从节点 slave-2 去复制性的主节点;
3. 待原来的主节点恢复后, 让它去复制新的主节点;
![20241229154732_FUe3W94T.webp](https://cdn.dong4j.site/source/image/20241229154732_FUe3W94T.webp)
4. 故障转移后的结构图
![20241229154732_znOFqeq4.webp](https://cdn.dong4j.site/source/image/20241229154732_znOFqeq4.webp)
## Redis Sentinel 功能
1. **监控:** Sentinel 节点会定期检测 Redis 数据节点和其余 Sentinel 节点是否可达;
2. **通知:** Sentinel 节点会将故障转移的结果通知给应用方;
3. **主节点故障转移:** 实现从节点晋升为主节点并维护后续正确的主从关系;
4. **配置提供者:** 客户端在初始化时, 连接 Sentinel 节点集群, 从中获取主节点信息;
## Redis Sentinel 安装与部署
下面将以 3 个 Sentinel 节点、1 个主节点、2 个从节点组成一个 Redis Sentinel 进行说明
![20241229154732_2SRjp4lI.webp](https://cdn.dong4j.site/source/image/20241229154732_2SRjp4lI.webp)
具体的物理部署:
| 角色 | ip | port | 别名 |
| :--------- | :-------- | :---- | :--------- |
| master | 127.0.0.1 | 6379 | 主节点 |
| slave-1 | 127.0.0.1 | 6380 | slave-1 |
| slave-2 | 127.0.0.1 | 6381 | slave-2 |
| sentinel-1 | 127.0.0.1 | 26379 | sentinel-1 |
| sentinel-2 | 127.0.0.1 | 26380 | sentinel-2 |
| sentinel-3 | 127.0.0.1 | 26381 | sentinel-3 |
**1. master 配置**
```
daemonize yes
dbfilename "6379.db"
dir "/Users/codeai/Develop/logs/redis/db/"
logfile "/Users/codeai/Develop/logs/redis/log/6379.log"
port 6379
requirepass 1234
```
**2. slave-1 配置**
```
daemonize yes
dbfilename "6380.db"
dir "/Users/codeai/Develop/logs/redis/db/"
logfile "/Users/codeai/Develop/logs/redis/log/6380.log"
port 6380
slaveof 127.0.0.1 6379
# 设置 master 验证密码
masterauth 1234
# 设置 slave 密码
requirepass 1234
```
**3. slave-2 配置**
```
daemonize yes
dbfilename "6381.db"
dir "/Users/codeai/Develop/logs/redis/db/"
logfile "/Users/codeai/Develop/logs/redis/log/6381.log"
port 6381
slaveof 127.0.0.1 6379
# 设置 master 验证密码
masterauth 1234
# 设置 slave 密码
requirepass 1234
```
**4. 启动 redis 服务**
```
redis-server redis-6379.conf; redis-server redis-6380.conf; redis-server redis-6381.conf
```
![20241229154732_gq307pmS.webp](https://cdn.dong4j.site/source/image/20241229154732_gq307pmS.webp)
**5. 确认主从关系**
主节点视角
```
redis-cli -h 127.0.0.1 -p 6379 info replication
# Replication
# 主节点
role:master
# 有 2 个 从节点
connected_slaves:2
# 从节点 1 信息
slave0:ip=127.0.0.1,port=6380,state=online,offset=112,lag=0
# 从节点 2 信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=112,lag=0
master_replid:8f43dc48cca779f46fa1516a38e24fb8c5423d94
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:112
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:112
```
从节点视角
```
redis-cli -h 127.0.0.1 -p 6380 info replication
# Replication
# 从节点
role:slave
# 主节点信息
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:238
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:8f43dc48cca779f46fa1516a38e24fb8c5423d94
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:238
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:238
```
**6. 部署 Sentinel 节点**
```
port 26379
daemonize yes
logfile "/Users/codeai/Develop/logs/redis/log/26379.log"
dir "/Users/codeai/Develop/logs/redis/db/"
# 监控 127.0.0.1:6379 主节点; 2 表示判断主节点失败至少需要 2 个 sentinel 节点同意
sentinel monitor mymaster 127.0.0.1 6379 2
# 设置监听的 master 密码
sentinel auth-pass mymaster 1234
# 30 秒内 ping 失败, sentinel 则认为 master 不可用
sentinel down-after-milliseconds mymaster 30000
# 在发生 failover 主备切换时,这个选项指定了最多可以有多少个 slave 同时对新的 master 进行同步
sentinel parallel-syncs mymaster 1
# 如果在该时间(ms)内未能完成 failover 操作,则认为该 failover 失败
sentinel failover-timeout mymaster 180000
```
其他节点只是端口不同
**7. 启动 sentinel 节点**
```
# 方式一
redis-sentinel redis-sentinel-26379.conf; redis-sentinel redis-sentinel-26380.conf; redis-sentinel redis-sentinel-26381.conf
# 方式二
redis-server redis-sentinel-26379.conf --sentinel; redis-server redis-sentinel-26380.conf --sentinel; redis-server redis-sentinel-26381.conf --sentinel;
```
![20241229154732_5ay2F1RC.webp](https://cdn.dong4j.site/source/image/20241229154732_5ay2F1RC.webp)
**8. 确认关系**
```
redis-cli -h 127.0.0.1 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
```
## 通过 Jedis 操作 Redis
[https://github.com/dong4j/redis-toolkit](https://github.com/dong4j/redis-toolkit)
包含 redis 配置文件, 搭建方式与单元测试
### Jedis 操作 Redis 的 三种方式
#### 单机模式
直接通过 JedisPool 操作 Redis
```java
Jedis jedis = null;
try {
// 从连接池获取一个 Jedis 实例
jedis = jedisPool.getResource();
jedis.set(key, value);
log.info(jedis.get(key));
} catch (Exception e) {
log.error("set error", e);
} finally {
if (null != jedis) {
// 释放资源还给连接池
jedis.close();
}
}
```
```java
try(Jedis jedis = jedisPool.getResource()) {
jedis.set(key, value);
log.info(jedis.get(key));
} catch (Exception e) {
log.error("set error", e);
}
```
#### 分片模式(ShardedJedis)
使用一致性哈希算法, 将 key 存储在对应实例中
```java
try(ShardedJedis jedis = shardedJedisPool.getResource();) {
jedis.set(key, value);
log.info(jedis.get(key));
} catch (Exception e) {
log.error("set error", e);
}
```
#### 集群模式(BinaryJedisCluster)
需要 Redis 3.0 以上才自带集群功能
### 集成 Jedis 的两种方式
1. 使用 spring-data-redis 集成 redis, 使用 RedisTemplate 或者 JedisConnectionFactory 获取 jedis 操作 Redis
2. 直接使用 JedisPool , ShardedJedisPool, ShardedJedisPool, ShardedJedisSentinelPool 获取 jedis 操作 Redis
#### spring-data-redis
#### 原生 jedis
##### JedisPool
##### ShardedJedisPool
#### 高可用的 Redis
##### JedisSentinelPool
##### ShardedJedisSentinelPool
代码详见: [https://github.com/dong4j/redis-toolkit](https://github.com/dong4j/redis-toolkit)
### 问题
```
All sentinels down, cannot determine where is mymaster master is running...
```
sentinel 安全模式默认是打开的, 又因为没有绑定可以访问的 ip 和设置访问密码,就不允许从外部访问;
redis 内部把 127 的地址转换成了 192.168.2.101 了,也就是你的本机的 ip 地址。所以访问 192 的地址就相当于从外部访问哨兵;
```
Cannot get master address from sentinel running @ 192.168.2.101:26379.
Reason: redis.clients.jedis.exceptions.JedisDataException: DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions:
1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent.
2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server.
3) If you started the server manually just for testing, restart it with the '--protected-mode no' option.
4) Setup a bind address or an authentication password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.. Trying next one
```
## [Shiro 引入后 Springboot 项目的循环依赖解决方案](https://blog.dong4j.site/posts/62ae7a01.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
最近在使用 Spingboot 做项目的时候,在引入 shiro 后,启动项目一直报错
```lua
Error creating bean with name 'debtServiceImpl': Bean with name 'debtServiceImpl' has been injected into other beans [repayBillServiceImpl,investServiceImpl,receiveBillServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
```
后来在网上找了半天说是依赖循环,检查了一下代码,确实存在循环依赖的现象,但是项目快要上线,再去改代码逻辑是来不及了,于是各种找解决方案,终于算是找到了。
首先说一下什么是依赖循环,比如:我现在有一个 ServiceA 需要调用 ServiceB 的方法,那么 ServiceA 就依赖于 ServiceB,那在 ServiceB 中再调用 ServiceA 的方法,就形成了循环依赖。Spring 在初始化 bean 的时候就不知道先初始化哪个 bean 就会报错。
```
public class ClassA {
@Autowired
ClassB classB;
}
public class ClassB {
@Autowired
ClassA classA;
}
```
那如何解决循环依赖,当然最好的方法是重构你的代码,进行解耦,但是重构不是一时的事情,那就使用下面的方法:
第一种:
```xml
```
在你的配置文件中,在互相依赖的两个 bean 的任意一个加上 lazy-init 属性。
第二种:
```java
@Autowired
@Lazy
private ClassA classA;
@Autowired
@Lazy
private ClassB classB;
```
在你注入 bean 时,在互相依赖的两个 bean 上加上@Lazy 注解也可以。
以上两种方法都能延迟互相依赖的其中一个 bean 的加载,从而解决循环依赖的问题。
## [SSH Config 那些你所知道和不知道的事](https://blog.dong4j.site/posts/ff561974.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
SSH(Secure Shell)是什么?是一项创建在应用层和传输层基础上的安全协议, 为计算机上的 Shell(壳层)提供安全的传输和使用环境.
也是专为远程登录会话和其他网络服务提供安全性的协议.
它能够有效防止远程管理过程中的信息泄露问题.
通过 SSH 可以对所有传输的数据进行加密, 也能够防止 DNS 欺骗和 IP 欺骗.
具体生成 SSH Key 方式请参考: [Github ssh key 生成, 免密登录服务器方法](https://deepzz.com/post/github-generate-ssh-key.html).
这里以 `id_ecdsa`(私钥) 和 `id_ecdsa.pub`(公钥) 为例.
本篇文章主要介绍 SSH 相关的使用技巧. 通过对 `~/.ssh/config` 文件的配置你可以大大简化 SSH 相关的操作, 如:
```bash
Host example # 关键词
HostName example.com # 主机地址
User root # 用户名
# IdentityFile ~/.ssh/id_ecdsa # 认证文件
# Port 22 # 指定端口
```
通过执行 `$ ssh example` 我就可以登录我的服务器. 而不需要敲更多的命令 `$ ssh root@example.com`.
又如我们想要向服务器传文件 `$ scp a.txt example:/home/user_name`. 比以前方便多了.
更过相关帮助文档请参考 `$ man ssh_config 5`.
### 配置项说明
SSH 的配置文件有两个:
```bash
$ ~/.ssh/config # 用户配置文件
$ /etc/ssh/ssh_config # 系统配置文件
```
下面来看看常用的配置参数.
**Host**
用于我们执行 SSH 命令的时候如何匹配到该配置.
- `*`, 匹配所有主机名.
- `*.example.com`, 匹配以 .example.com 结尾.
- `!*.dialup.example.com,*.example.com`, 以 ! 开头是排除的意思.
- `192.168.0.?`, 匹配 192.168.0.[0-9] 的 IP.
**AddKeysToAgent**
是否自动将 key 加入到 `ssh-agent`, 值可以为 no(default)/confirm/ask/yes.
如果是 yes, key 和密码都将读取文件并以加入到 agent , 就像 `ssh-add`. 其他分别是询问、确认、不加入的意思. 添加到 ssh-agent 意味着将私钥和密码交给它管理,
让它来进行身份认证.
**AddressFamily**
指定连接的时候使用的地址族, 值可以为 any(default)/inet(IPv4)/inet6(IPv6).
**BindAddress**
指定连接的时候使用的本地主机地址, 只在系统有多个地址的时候有用. 在 UsePrivilegedPort 值为 yes 的时候无效.
**ChallengeResponseAuthentication**
是否响应支持的身份验证 chanllenge, yes(default)/no.
**Compression**
是否压缩, 值可以为 no(default)/yes.
**CompressionLevel**
压缩等级, 值可以为 1(fast)-9(slow). 6(default), 相当于 gzip.
**ConnectionAttempts**
退出前尝试连接的次数, 值必须为整数, 1(default).
**ConnectTimeout**
连接 SSH 服务器超时时间, 单位 s, 默认系统 TCP 超时时间.
**ControlMaster**
是否开启单一网络共享多个 session, 值可以为 no(default)/yes/ask/auto. 需要和 ControlPath 配合使用, 当值为 yes 时, ssh 会监听该路径下的
control socket, 多个 session 会去连接该 socket, 它们会尽可能的复用该网络连接而不是重新建立新的.
**ControlPath**
指定 control socket 的路径, 值可以直接指定也可以用一下参数代替:
- %L 本地主机名的第一个组件
- %l 本地主机名(包括域名)
- %h 远程主机名(命令行输入)
- %n 远程原始主机名
- %p 远程主机端口
- %r 远程登录用户名
- %u 本地 ssh 正在使用的用户名
- %i 本地 ssh 正在使用 uid
- %C 值为 %l%h%p%r 的 hash
请最大限度的保持 ControlPath 的唯一. 至少包含 %h, %p, %r(或者 %C).
**ControlPersist**
结合 ControlMaster 使用, 指定连接打开后后台保持的时间. 值可以为 no/yes / 整数, 单位 s. 如果为 no, 最初的客户端关闭就关闭. 如果 yes/0, 无限期的,
直到杀死或通过其它机制, 如: ssh -O exit.
**GatewayPorts**
指定是否允许远程主机连接到本地转发端口, 值可以为 no(default)/yes. 默认情况, ssh 为本地回环地址绑定了端口转发器.
**HostName**
真实的主机名, 默认值为命令行输入的值(允许 IP). 你也可以使用 %h, 它将自动替换, 只要替换后的地址是完整的就 ok.
**IdentitiesOnly**
指定 ssh 只能使用配置文件指定的 identity 和 certificate 文件或通过 ssh 命令行通过身份验证, 即使 ssh-agent 或 PKCS11Provider 提供了多个
identities. 值可以为 no(default)/yes.
**IdentityFile**
指定读取的认证文件路径, 允许 DSA, ECDSA, Ed25519 或 RSA. 值可以直接指定也可以用一下参数代替:
- %d, 本地用户目录 ~
- %u, 本地用户
- %l, 本地主机名
- %h, 远程主机名
- %r, 远程用户名
**LocalCommand**
指定在连接成功后, 本地主机执行的命令(单纯的本地命令). 可使用 %d, %h, %l, %n, %p, %r, %u, %C 替换部分参数. 只在 PermitLocalCommand 开启的情况下有效.
**LocalForward**
指定本地主机的端口通过 ssh 转发到指定远程主机. 格式: LocalForward [bind_address:]post host:hostport, 支持 IPv6.
**PasswordAuthentication**
是否使用密码进行身份验证, yes(default)/no.
**PermitLocalCommand**
是否允许指定 LocalCommand, 值可以为 no(default)/yes.
**Port**
指定连接远程主机的哪个端口, 22(default).
**ProxyCommand**
指定连接的服务器需要执行的命令. %h, %p, %r
如: ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
**User**
登录用户名
### 相关技巧
#### 管理多组密钥对
有时候你会针对多个服务器有不同的密钥对, 每次通过指定 `-i` 参数也是非常的不方便. 比如你使用 github 和 coding.
那么你需要添加如下配置到 `~/.ssh/config`:
```bash
Host github
HostName %h.com
IdentityFile ~/.ssh/id_ecdsa_github
User git
Host coding
HostName git.coding.net
IdentityFile ~/.ssh/id_rsa_coding
User git
```
当你克隆 coding 上的某个仓库时:
```bash
# 原来
$ git clone git@git.coding.net:deepzz/test.git
# 现在
$ git clone coding:deepzz/test.git
```
#### vim 访问远程文件
vim 可以直接编辑远程服务器上的文件:
```bash
$ vim scp://root@example.com//home/centos/docker-compose.yml
$ vim scp://example//home/centos/docker-compose.yml
```
#### 远程服务当本地用
通过 LocalForward 将本地端口上的数据流量通过 ssh 转发到远程主机的指定端口. 感觉你是使用的本地服务, 其实你使用的远程服务. 如远程服务器上运行着
Postgres, 端口 5432(未暴露端口给外部). 那么, 你可以:
```bash
Host db
HostName db.example.com
LocalForward 5433 localhost:5432
```
当你连接远程主机时, 它会在本地打开一个 5433 端口, 并将该端口的流量通过 ssh 转发到远程服务器上的 5432 端口.
首先, 建立连接:
```bash
$ ssh db
```
之后, 就可以通过 Postgres 客户端连接本地 5433 端口:
```bash
$ psql -h localhost -p 5433 orders
```
#### 多连接共享
什么是多连接共享?在你打开多个 shell 窗口时需要连接同一台服务器, 如果你不想每次都输入用户名, 密码, 或是等待连接建立,
那么你需要添加如下配置到 `~/.ssh/config`:
```bash
ControlMaster auto
ControlPath /tmp/%r@%h:%p
```
#### 禁用密码登录
如果你对服务器安全要求很高, 那么禁用密码登录是必须的. 因为使用密码登录服务器容易受到暴力破解的攻击, 有一定的安全隐患.
那么你需要编辑服务器的系统配置文件 `/etc/ssh/sshd_config`:
```bash
PasswordAuthentication no
ChallengeResponseAuthentication no
```
#### 关键词登录
为了更方便的登录服务器, 我们也可以省略用户名和主机名, 采用关键词登录. 那么你需要添加如下配置到 `~/.ssh/config`:
```bash
Host deepzz # 别名
HostName deepzz.com # 主机地址
User root # 用户名
# IdentityFile ~/.ssh/id_ecdsa # 认证文件
# Port 22 # 指定端口
```
那么使用 `$ ssh deepzz` 就可以直接登录服务器了.
#### 代理登录
有的时候你可能没法直接登录到某台服务器, 而需要使用一台中间服务器进行中转, 如公司内网服务器. 首先确保你已经为服务器配置了公钥访问, 并开启了
agent forwarding, 那么你需要添加如下配置到 `~/.ssh/config`:
```bash
Host gateway
HostName proxy.example.com
User root
Host db
HostName db.internal.example.com # 目标服务器地址
User root # 用户名
# IdentityFile ~/.ssh/id_ecdsa # 认证文件
ProxyCommand ssh gateway netcat -q 600 %h %p # 代理命令
```
那么你现在可以使用 `$ ssh db` 连接了.
## [内存优化实战:从数据结构选择到GC策略](https://blog.dong4j.site/posts/b2f99cee.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
### 内存优化对比
**数据量**
> 外呼名单 10 万
> 白名单 100 万
> 黑名单 500 万
**JVM 参数**
```
-verbose:gc
-XX:+HeapDumpOnOutOfMemoryError
-server
-Xms1g
-Xmx1g
-XX:PermSize=512m
-XX:SurvivorRatio=2
-XX:+UseParallelGC
```
#### 优化之前
**对比耗时**
```
2018-05-31 15:29:49 [INFO]] [pool-1-thread-1] [parseRegulation] 白名单匹配个数 = 25
2018-05-31 15:29:50 [INFO]] [pool-1-thread-1] [parseRegulation] 黑名单匹配个数 = 189
StopWatch '': running time (millis) = 906
-----------------------------------------
ms % Task name
-----------------------------------------
00906 100% 对比
...
diffList.size = 99810
telNos.size = 99810
```
**内存消耗**
启动时第一次运行解析任务, 并且符合条件的 calloutList 为 99810;
最高使用 `762.5` M
![20241229154732_ky3i189B.webp](https://cdn.dong4j.site/source/image/20241229154732_ky3i189B.webp)
持续运行一段时间, 并且使用相同的号码包进行测试的结果
发生了 OOM
![20241229154732_jpeRRWzH.webp](https://cdn.dong4j.site/source/image/20241229154732_jpeRRWzH.webp)
#### 优化之后
**对比耗时**
```
2018-05-31 15:46:45 [INFO]] [pool-1-thread-1] [dealWhiteAndBlackList] 白名单匹配个数 = 25
2018-05-31 15:46:45 [INFO]] [pool-1-thread-1] [dealWhiteAndBlackList] 白名单匹配个数 = 189
StopWatch '': running time (millis) = 109
-----------------------------------------
ms % Task name
-----------------------------------------
00109 100% 对比
...
diffList.size = 99810
telNos.size = 99810
```
**内存消耗**
启动时第一次运行解析任务, 并且符合条件的 calloutList 为 99810;
最高 `728` M
![20241229154732_898V9sAS.webp](https://cdn.dong4j.site/source/image/20241229154732_898V9sAS.webp)
持续运行一段时间, 并且使用相同的号码包进行测试的结果
![20241229154732_sb34vgzR.webp](https://cdn.dong4j.site/source/image/20241229154732_sb34vgzR.webp)
#### 优化方案
1. 使用 BloomFilter 代替 DataCache 来存储黑白名单;
2. 及时清理占用大内存的临时变量;
##### 布隆过滤器
![20241229154732_PYx6qUxn.webp](https://cdn.dong4j.site/source/image/20241229154732_PYx6qUxn.webp)
**简介:**
是一个很长的二进制向量和一系列随机映射函数. 布隆过滤器可以用于检索一个元素是否在一个集合中. 它的优点是空间效率和查询时间都远远超过一般的算法,
缺点是有一定的误识别率和删除困难.
**原理:**
当一个元素被加入集合时, 通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点, 把它们置为 1. 检索时,
我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了: 如果这些点有任何一个 0, 则被检元素一定不在;如果都是 1, 则被检元素很可能在.
**优点:**
相比于其它的数据结构, 布隆过滤器在空间和时间方面都有巨大的优势. 布隆过滤器存储空间和插入/查询时间都是常数(O(k)). 而且它不存储元素本身,
在某些对保密要求非常严格的场合有优势.
**缺点**:
一定的误识别率和删除困难.
#### 开发建议
程序的运行会直接影响系统环境的变化, 从而影响 GC 的触发. 若不针对 GC 的特点进行设计和编码, 就会出现内存驻留等一系列负面影响.
为了避免这些影响, 基本的原则就是尽可能地减少垃圾和减少 GC 过程中的开销. 具体措施包括以下几个方面:
1. 不要显式调用 `System.gc()`
此函数建议 JVM 进行主 GC, 虽然只是建议而非一定, 但很多情况下它会触发主 GC, 从而增加主 GC 的频率, 也即增加了间歇性停顿的次数.
2. 尽量减少临时对象的使用
临时对象在跳出函数调用后, 会成为垃圾, 少用临时变量就相当于减少了垃圾的产生.
3. 对象不用时最好显式置为 null
一般而言, 为 null 的对象都会被作为垃圾处理, 所以将不用的对象显式地设为 null, 有利于 GC 收集器判定垃圾, 从而提高了 GC 的效率.
4. 尽量少用静态对象变量
静态变量属于全局变量, 不会被 GC 回收, 它们会一直占用内存.
## [如何实现优雅的重试机制?两种方式大比拼!](https://blog.dong4j.site/posts/2c5b3e3a.md)
![random-pic-api](https://api.dong4j.ink:1024/cover?spm={{spm}})
文/LNAmp(简书作者)
原文链接: [http://www.jianshu.com/p/80c7777d48ad](http://www.jianshu.com/p/80c7777d48ad)
## 解决方案演化
这个问题的技术点在于能够触发重试, 以及重试情况下逻辑有效执行.
### 解决方案一: try-catch-redo 简单重试模式
包装正常上传逻辑基础上, 通过判断返回结果或监听异常决策是否重试, 同时为了解决立即重试的无效执行 (假设异常是有外部执行不稳定导致的),
休眠一定延迟时间重新执行功能逻辑.
```java
public void commonRetry(Map dataMap) throws InterruptedException {
Map paramMap = Maps.newHashMap();
paramMap.put("tableName", "creativeTable");
paramMap.put("ds", "20160220");
paramMap.put("dataMap", dataMap);
boolean result = false;
try {
result = uploadToOdps(paramMap);
if (!result) {
Thread.sleep(1000);
uploadToOdps(paramMap); // 一次重试
}
} catch (Exception e) {
Thread.sleep(1000);
uploadToOdps(paramMap);// 一次重试
}
}
```
### 解决方案二: try-catch-redo-retry strategy 策略重试模式
述方案还是有可能重试无效, 解决这个问题尝试增加重试次数 retrycount 以及重试间隔周期 interval, 达到增加重试有效的可能性.
```java
public void commonRetry(Map dataMap) throws InterruptedException {
Map paramMap = Maps.newHashMap();
paramMap.put("tableName", "creativeTable");
paramMap.put("ds", "20160220");
paramMap.put("dataMap", dataMap);
boolean result = false;
try {
result = uploadToOdps(paramMap);
if (!result) {
reuploadToOdps(paramMap,1000L,10);// 延迟多次重试
}
} catch (Exception e) {
reuploadToOdps(paramMap,1000L,10);// 延迟多次重试
}
}
```
方案一和方案二存在一个问题: 正常逻辑和重试逻辑强耦合, 重试逻辑非常依赖正常逻辑的执行结果, 对正常逻辑预期结果被动重试触发, 对于重试根源往往由于逻辑复杂被淹没,
可能导致后续运维对于重试逻辑要解决什么问题产生不一致理解. 重试正确性难保证而且不利于运维, 原因是重试设计依赖正常逻辑异常或重试根源的臆测.
## 优雅重试方案尝试
那有没有可以参考的方案实现正常逻辑和重试逻辑解耦, 同时能够让重试逻辑有一个标准化的解决思路?答案是有: 那就是基于代理设计模式的重试工具,
我们尝试使用相应工具来重构上述场景.
### 尝试方案一: 应用命令设计模式解耦正常和重试逻辑
命令设计模式具体定义不展开阐述, 主要该方案看中命令模式能够通过执行对象完成接口操作逻辑, 同时内部封装处理重试逻辑, 不暴露实现细节,
对于调用者来看就是执行了正常逻辑, 达到解耦的目标, 具体看下功能实现. (类图结构)
![20241229154732_eAN1LD66.webp](https://cdn.dong4j.site/source/image/20241229154732_eAN1LD66.webp)
IRetry 约定了上传和重试接口, 其实现类 OdpsRetry 封装 ODPS 上传逻辑, 同时封装重试机制和重试策略. 与此同时使用 recover 方法在结束执行做恢复操作.
而我们的调用者 LogicClient 无需关注重试, 通过重试者 Retryer 实现约定接口功能, 同时 Retryer 需要对重试逻辑做出响应和处理, Retryer
具体重试处理又交给真正的 IRtry 接口的实现类 OdpsRetry 完成. 通过采用命令模式, 优雅实现正常逻辑和重试逻辑分离, 同时通过构建重试者角色,
实现正常逻辑和重试逻辑的分离, 让重试有更好的扩展性.
### 尝试方案二: spring-retry 规范正常和重试逻辑
[spring](http://lib.csdn.net/base/javaee "Java EE知识库")-retry 是一个开源工具包, 目前可用的版本为 1.1.2.RELEASE, 该工具把重试操作模板定制化,
可以设置重试策略和回退策略. 同时重试执行实例保证线程安全, 具体场景操作实例如下:
```java
public void upload(final Map map) throws Exception {
// 构建重试模板实例
RetryTemplate retryTemplate = new RetryTemplate();
// 设置重试策略, 主要设置重试次数
SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections., Boolean> singletonMap(Exception.class, true));
// 设置重试回退操作策略, 主要设置重试间隔时间
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(100);
retryTemplate.setRetryPolicy(policy);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
// 通过 RetryCallback 重试回调实例包装正常逻辑逻辑, 第一次执行和重试执行执行的都是这段逻辑
final RetryCallback