开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

面试官就是要问我SpringMVC的源码

发表于 2021-06-11

《对线面试官》系列目前已经连载22篇啦!有深度风趣的系列!

  • 【对线面试官】Java注解
  • 【对线面试官】Java泛型
  • 【对线面试官】 Java NIO
  • 【对线面试官】Java反射 && 动态代理
  • 【对线面试官】多线程基础
  • 【对线面试官】 CAS
  • 【对线面试官】synchronized
  • 【对线面试官】AQS&&ReentrantLock
  • 【对线面试官】线程池
  • 【对线面试官】ThreadLocal
  • 【对线面试官】CountDownLatch和CyclicBarrier
  • 【对线面试官】List
  • 【对线面试官】Map
  • 【对线面试官】SpringMVC
  • 【对线面试官】Spring基础
  • 【对线面试官】SpringBean生命周期
  • 【对线面试官】Redis基础
  • 【对线面试官】Redis持久化
  • 【对线面试官】Kafka基础
  • 【对线面试官】使用Kafka会考虑什么问题?
  • 【对线面试官】MySQL索引

文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的。

鉴于很多同学反馈没看懂【对线面试官】系列,基础相关的知识我确实写过文章讲解过啦,但有的同学就是不爱去翻。

为了让大家有更好的体验,我把基础文章也找出来(重要的知识点我还整理过电子书,比如说像多线程、集合这种面试必考的早就已经转成PDF格式啦)

我把这些上传到网盘,你们有需要直接下载就好了。做到这份上了,不会还想白嫖吧?点赞和转发又不用钱。

链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom

欢迎关注我的微信公众号【Java3y】来聊聊Java面试

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何吃透一个 Java 项目?

发表于 2021-06-11

先说一下大多数新手的情况:就是对着视频敲 Java 项目,其中遇到的 BUG 还能解决,但就是每次敲完一个项目,就感觉很空虚,项目里面的知识点感觉懂了但又好像没懂,应该怎样才能掌握一个项目所用的知识点呢?

先分享一位好朋友丁威的经验吧,他是《RocketMQ技术内幕》一书的作者。他在尝试学习 RocketMQ 之前未曾接触过消息中间件,但硬是通过自己提炼的学习方法,最终成为 RocketMQ 社区的优秀布道师。这让他有了一个非常亮眼的标签,极大提高了职场竞争力。

他的总结有以下四点:

  • 了解这个项目的使用场景、以及架构设计中将承担的责任。
  • 寻找官方文档,从整体上把握这个项目的设计理念。
  • 搭建自己的开发调试环境,运行官方提供 Demo 示例,为后续深入研究打下基础。
  • 先主干流程再分支流程,注意切割,逐个击破。

我认为是非常有道理的。记得我一开始参加工作的时候,拿到领导安排的项目后,非常懵逼,完全不知道该如何下手,不知道自己该干嘛。去问领导,领导说没时间,要我自己先研究研究,研究什么呢?

一开始当然毫无头绪,后面就慢慢琢磨出来了一点小心得。

先从项目的需求文档下手,先了解这个项目是干嘛的,了解个大概,就不慌了。然后把这个项目的源码在本地跑起来,“跑起来”,说着简单,真正做的时候还是挺难的,我记得我当时跑第一个项目差不多用了一周多的时间,因为我的开发环境和项目要求的有一些不一样,就导致出了很多问题。

我印象很深刻的问题有两个,第一个就是乱码了,类似下图这种。


第二个就是编译出错,原因很简单,JDK 的版本不一样。

这两个问题虽然简单,但特别常见,真的,新手基本上都会遇到。一开始遇到的时候是很慌的,因为源码肯定不会有问题,有问题的肯定是自己的开发环境。

折腾了一周多的时间,项目终于跑起来了。

我让自己体验了一把测试的角色,就是把能操作的按钮全点一遍,体验一下项目哪些已经开发完成了,哪些还没有。没用多久,我就测出来了两三个新 bug,基本上都是一些非常规操作引起的,虽然我没能解决,但报告给领导后,领导还是很惊讶的,他看我的小眼神,多少有点看吴下阿蒙的感觉,“小伙子,有长进啊!”

然后,我并没有着急去开发领导安排给我的任务,而是去看同事已经完成的代码,当然是带着问题去看的,为什么他会这么写?这么写的好处是什么?如果换做是我,我会怎么实现?这其中的差别是我自己考虑不周吗?

当然了,我不会看太细致,因为有些内容超出了我的技术范围,我看不懂的,只能是“走马观花”,大致看明白是什么意思就“得过且过”了。

这里提醒大家一点的是,千万不要陷入技术细节,尤其是一开始做项目的时候,因为自己不了解业务,再加上技术水平可能达不到,研究技术细节很容易就阵亡了。

另外一点就是,基本功一定要扎实,不要连整体的代码脉络都看不懂,那样就很吃力了。我这里有一份 GitHub 上星标 115k+ 的 Java 教程,里面涵盖了 Java 所有的知识点,包括 Java 语法、Java 集合框架、Java IO、Java 并发编程和 Java 虚拟机,内容不多,只讲重点。

GitHub 星标 115k+的 Java 教程,超级硬核!

尽量花一点时间巩固一下基础,不要让自己太被动。遇到自己生疏的知识点,就主动去查一下。

看源码的时候,自己加一些日志啊,加一些断点啊,去跑一跑。有时候,不加断点,只是去看源码很可能会忽略掉一些关键的技术细节,因为 Java 是多态的,有些地方看源码只能看到类和类之间的层次关系,却看不到子类是怎么重写父类方法的,或者实现类是怎么实现接口的,通过调试就可以看的很明白。

还有一些分支语句啊,如果只是看源码可能不明白进入这个分支的条件是什么。还有一些比较关键的技术细节啊,只看源码可能懵懵懂懂的,跟着断点一步步地深入可能就全明白了。

举个例子,拿 StringBuilder 的 append() 方法来说吧。StringBuilder 的内部是通过字符数组(Java 8)实现的, 如果新添加的字符串长度超出了数组的长度,是要进行扩容的。加了断点后,遇到需要扩容的情况就能看到 JDK 的内部是怎么来进行扩容的啦,如下图所示。


一般来说,项目都是比较庞大的,代码会比较多,这时候最好只关注一个模块,就是自己通过需求文档,或者阅读源码了解最多的模块,尽量把这个模块的整体逻辑给吃透了,如果领导恰好安排你做的这个模块的业务代码,那你的上手难度就大大降低了!

再总结一下:

  • 拿到项目后,不要着急动手,先看文档,尤其是需求文档。
  • 把项目的源码在本地跑起来。
  • 做一些测试,看能不能测出一些边界的 bug。
  • 关注一个模块,去研究一下源码,但不要过于深究技术细节。

我再给大家推荐两个优秀的 Java 开源项目吧,如果你还没有参加工作,可以拿这两个项目作为练手项目。

  • SpringBoot 完整电商系统 Mall:包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。
  • vhr:微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发。

我之前做的一个电商项目还用了 mall 作为基层的架构!mall 的学习教程非常全面,作者直接做了一个在线的网站,基本上用的技术都讲述得特别明白!

vhr 目前在 GitHub 上已经有 20.8k 的 star,属于很强的那种了。

后端技术栈

  • Spring Boot
  • Spring Security
  • MyBatis
  • MySQL
  • Redis
  • RabbitMQ
  • Spring Cache
  • WebSocket
    …

前端技术栈

  • Vue
  • ElementUI
  • axios
  • vue-router
  • Vuex
  • WebSocket
  • vue-cli4
    …

基本上主流的前后端技术都用到了。作者为初学者特意录了项目的部署视频教程,很贴心~

文档写得特别全,我简单列举下。

  • 权限数据库设计
  • 服务端环境搭建
  • 动态处理角色和资源的关系
  • 密码加密并加盐
  • 服务端异常的统一处理
  • axios 请求封装,请求异常统一处理
  • 将请求方法挂到Vue上
  • 登录状态的保存
  • 登录成功后动态加载组件
  • 角色资源关系管理
  • 用户角色关系管理

基本上每个知识点都有对应的文档,文档齐全的话,对吃透项目是极好的帮助。

嗯,最后强调一点,大家在做练手项目的时候,一定记得写注释。我在很多地方看到这样一个观点,“请停止写注释,因为只有烂的代码才需要注释。”这个观点非常巧妙,它让我想起了孟子的一句话,“杨氏为我,是无君也;墨氏兼爱,是无父也。无父无君,是禽兽也。”

Java 源码的作者绝对是这个世界上最优秀的程序员,连他们都写注释,那些声称“请停止写注释”的号召者是不是要啪啪啪地打脸,直到打肿为止。

拿 String 来说吧,要了解这个类,直接看类的注释就足够了,写得非常详细。

一个优秀的 Java 项目也应该是这样的,注释必须到位,不然别人怎么吃透,吃不透的。只能说一句,Java 源码的作者,yyds(阴阳大师,hhh)。

我是二哥呀,赞一个可好?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 Cloud Feign 初始化配置流程

发表于 2021-06-10

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

文章目的 :

  • Feign 主要流程源码分析
  • Feign 的要点分析
  • Feign 的设计思路及扩散思考

二 . 源码梳理

以一个最基础的 OpenFeign 的案例为例 , 我们在使用时通常会有如下操作 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// Step 1: 开启 Feign 客户端
@EnableFeignClients
@SpringBootApplication
public class ComGangCloudTemplateOrderApplication {..........}


// Step 2 : 准备 FeignClient 对象
@Component
@FeignClient("product-server")
public interface ProductFeignClient {
@GetMapping("/template/get")
CloudTemplateEntity get(@RequestParam("desc") String desc);
}

// Step 3 : 调用 FeignClient 对象
@Autowired
private ProductFeignClient productFeignClient;
productFeignClient.get("order-server")

光看这三个步骤 , 大概可以得出几个问题 :

  1. EnableFeignClients 的作用 ?
  2. @FeignClient 的扫描

2.1 通过 @EnableFeignClients 开启 FeignClients

主要的配置集中在 FeignClientsRegistrar 中 , 主要包括以下内容 :

首先看一下 FeignClientsRegistrar 的调用流程:

  • Step 1 : Bean 加载时调用 registerBeanDefinitions 完成 BeanDefinition 的注册
  • Step 2 : registerDefaultConfiguration 注册 config
  • Step 3 : 扫描 FeignClient , 并且进行注册
  • Step 4 : 注册扫描的所有的 FeignClient

Step 1 : Bean 加载时调用 registerBeanDefinitions 完成 BeanDefinition 的注册

1
2
3
4
5
6
7
8
java复制代码public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 其中涉及到2个主要的步骤 :
// 1.注册Configuration
registerDefaultConfiguration(metadata, registry);
// 2. 注册 Feign Client
registerFeignClients(metadata, registry);
}

Step 2 : registerDefaultConfiguration 注册 config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 1. 获取注解上面的属性 -> PS:001
Map<String, Object> defaultAttrs =
metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

// 2. 注册 BeanDefinitionBuilder (PS : 这一步骤实际上构建了一个 BeanDefinitionBuilder)
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {

// BeanDefinitionBuilder是使用构建器模式构建BeanDefinitions
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
// name : default.com.gang.cloud.template.demo.ComGangCloudTemplateOrderApplication
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
// default.com.gang.cloud.template.demo.ComGangCloudTemplateOrderApplication.FeignClientSpecification
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition()
);

}

PS:001 defaultAttrs 参数

image.png

Step 3 : 扫描 FeignClient

整体大纲就是 :

  1. 从 EnableFeignClients 获取属性 attrs
  2. 通过属性 attrs 获取 基础扫描路径 basePackages
  3. 扫描 basePackages 下面的所有标注 @Component 的类
  4. 获取其中的 @FeignClient
  5. 对注解标注的类进行 registry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// 注解类的使用主要是 FeignClientsRegistrar
C05- FeignClientsRegistrar(AnnotationMetadata metadata,BeanDefinitionRegistry registry)
M5_01- registerFeignClients : FeignClient 核心方式
- 准备对象 ClassPathScanningCandidateComponentProvider
- 获取标注了 EnableFeignClients 的 Map 集合
- 构建一个 AnnotationTypeFilter
?- 为什么不和上面一样使用 getAnnotationAttributes 直接获取相关类 -> PS:M5_01_01
- getBasePackages 获取需要扫描的路径集合 Set<String>->LV001
FOR- 循环路径集合 : LV001
- findCandidateComponents 获取对应的 Set<BeanDefinition>->LV002
?- 注意 , 这里会通过 findCandidateComponents 查找路径下标注了 Component 的类型 -> PS:M5_01_02
FOR- 循环 BeanDefinition : LV002
- 获取 FeignClient 的属性 Map<String, Object> ->LV003
- registerClientConfiguration 注册配置信息
- registerFeignClient 注册当前 FeignClient
M5_02- registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes)
?- 核心作用就是构建一个 BeanDefined , 其中又主要可以分为4步 -> PS:M5_02_01
1- BeanDefinitionBuilder.genericBeanDefinition 构建一个 BeanDefinitionBuilder
2- definition.getBeanDefinition() 构建一个 AbstractBeanDefinition
3- new BeanDefinitionHolder 构建一个 BeanDefinitionHolder
4- BeanDefinitionReaderUtils.registerBeanDefinition 注入 Bean

以上是主流程 ,但是我们还是可以看一下其中的一点小细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码// PS : 其中省略了部分代码 , 想看完整版的可以看源码
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 准备 scan 对象用于扫描
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);

// 这一句很重要 , 这是个用于 Scan 的排除筛选器 ,他会屏蔽调无用的 Bean
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);

if (clients == null || clients.length == 0) {
// 添加排除筛选器 -> PS:0002
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
} else {
// 省略 ,同样是为了进行特殊的匹配
}

for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
// 获取注解属性 -> PS:0003
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 注册 FeignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}

// PS:0002 排除筛选器的使用
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.includeFilters) {
//..........
}
}

Step 3 : 扫描 FeignClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();

// 准备 BeanDefinitionBuilder -> org.springframework.cloud.openfeign.FeignClientFactoryBean
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// 校验属性是否合法
validate(attributes);

// 省略所有的 addPropertyValue 操作 ,此操作为 definition 添加属性

String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

// 由于有一个默认 BeanDefinition , 此处通过 primary 进行覆盖
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);

BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
// 向给定的bean工厂注册给定的bean定义
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

PS : 这个对象在创建Bean过程中会被调用 ,我们后面再说!!!

2.2 FeignAutoConfiguration 的配置

Feign 其实是支持 OKHttp 方式调用的 ,该方法在 FeignAutoConfiguration 中进行配置 , 该配置类中提供了2个连接框架 : HttpClientFeignConfiguration / OkHttpFeignConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码// HttpClientFeignConfiguration 的配置项
@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory,FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
// 最大连接数
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
// 连接存活时间和单位
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);

this.connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000, httpClientProperties.getConnectionTimerRepeat());

return connectionManager;
}

@Bean
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {

RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects())
.build();

this.httpClient = httpClientFactory.createBuilder()
.setConnectionManager(httpClientConnectionManager)
.setDefaultRequestConfig(defaultRequestConfig).build();

return this.httpClient;
}


// OkHttpFeignConfiguration 的配置项
@Bean
@ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(
FeignHttpClientProperties httpClientProperties,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
// OKHttp 连接池属性
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}

@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
ConnectionPool connectionPool,
FeignHttpClientProperties httpClientProperties) {

Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.followRedirects(followRedirects).connectionPool(connectionPool)
.build();

return this.okHttpClient;
}

那么问题来了 : 如何切换到 OKHttp 呢?

PS : 网上有一种方法 ,通过配置 okhttp3.OkHttpClient 的方法 , 但是经过个人测试 , 可能由于版本不同会出现问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码// 如果细看源码 , 会发现存在2个 OkHttpFeignConfiguration 
//一个在 FeignAutoConfiguration 内部 , 另外一个存在于 org.springframework.cloud.openfeign.clientconfig 包下

// 疑点 : 点开FeignAutoConfiguration$OkHttpFeignConfiguration 上面有多个 Conditional

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
protected static class OkHttpFeignConfiguration {
//............
}

// [Pro] : 通过 OnClassCondition 测试的时候就会发现 , 由于 ILoadBalancer 存在 , 则不会加载该类下的配置

而在 org.springframework.cloud.openfeign.clientconfig 包下的配置中 , 则有如下要求
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)

// 所以 , 已知的那种配置实际上会导致2个配置项都不会走



//!!! 那么应该怎么配置 ?



// Step 1 :添加 eign-okhttp 相关配置
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>


// Step 2 : Application 上导入配置文件
@Import(value = {OkHttpFeignConfiguration.class})


// Step 3 : config 中注册 Client
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureAfter(value = {FeignAutoConfiguration.class, OkHttpFeignConfiguration.class})
public class FeignOkHttpConfig {

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(okhttp3.OkHttpClient client) {
// 这里还有相关配置 ,暂时省略 , 写流程的时候补上
return new OkHttpLoadBalancingClient(....);
}
}

总结

Feign 的配置篇基本上就说完了 , 后面会说明一个 FeignBean 的创建和一个 Invoke 流程

附录

PS:0003 attributes 中包含哪些属性?

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码C- FeignClient
M- String value() default ""
M- String serviceId() default ""
M- String contextId() default "" : 如果存在,它将用作bean名而不是名称,但不会用作服务id。
M- String name() default ""
M- String qualifier() default ""
M- String url() default ""
M- boolean decode404() default false
M- Class<?>[] configuration() : 为虚客户端定制的配置类。
M- Class<?> fallback()
M- Class<?> fallbackFactory()
M- String path() default ""
M- boolean primary() default true

常见的 FeignClient 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 覆盖默认配置
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
link : https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-overriding-defaults

// 获取回退的原因
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)

// 获取触发回退的原因
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",fallbackFactory = TestFallbackFactory.class)

// 配置主 Bean
// PS : 当使用 Feign 和 Spring Cloud CircuitBreaker 回退时,在同一类型的 ApplicationContext 中有多个 bean
@FeignClient(name = "hello", primary = false)

Application 配置篇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
java复制代码feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
metrics.enabled: false


// 已经配套的 Bean 类 ,Debug get/set 就可以看到使用的节点
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {

// 禁用SSL验证的默认值
public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;

// 最大od连接数的缺省值
public static final int DEFAULT_MAX_CONNECTIONS = 200;

// 每条路由的最大连接数的缺省值
public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;

// 存活的时间的默认值
public static final long DEFAULT_TIME_TO_LIVE = 900L;

// 默认存活时间单位.
public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;

// 是否重定向
public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;

// 连接超时的默认值
public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;

//连接计时器重复的默认值
public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;

private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;

private int maxConnections = DEFAULT_MAX_CONNECTIONS;

private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;

private long timeToLive = DEFAULT_TIME_TO_LIVE;

private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;

private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;

private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;

private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;

}

@ConfigurationProperties("feign.client")
public class FeignClientProperties {

private boolean defaultToProperties = true;
private String defaultConfig = "default";
private Map<String, FeignClientConfiguration> config = new HashMap<>();

}

public static class FeignClientConfiguration {

private Logger.Level loggerLevel;

private Integer connectTimeout;

private Integer readTimeout;

private Class<Retryer> retryer;

private Class<ErrorDecoder> errorDecoder;

private List<Class<RequestInterceptor>> requestInterceptors;

private Boolean decode404;

private Class<Decoder> decoder;

private Class<Encoder> encoder;

private Class<Contract> contract;

private ExceptionPropagationPolicy exceptionPropagationPolicy;

//........

}

// Step 1 : Config 配置的类
FeignClientConfiguration
FeignHttpClientProperties
FeignClientProperties


// Step 2 : 配置的位置
C- FeignClientFactoryBean
M- configureUsingProperties
M- configureUsingConfiguration

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

公司没GIS开发,让我上 前言 正文 总结:

发表于 2021-06-10

如果你真的愿意去努力,你人生最坏的结果,也不过是大器晚成。

前言

对于一个从没搞过GIS开发的人来说,一开始真的是无从下手,网上搜索各种资源,最终…

地图发布肯定要有一个地图服务器啊,于是找到了GeoServer地图服务器,地图来源呢,找到了OpeStreetMap(简称OSM,中文是公开地图)是一个网上地图协作计划,目标是创造一个内容自由且能让所有人编辑的世界地图。

用啥存储地图数据呢,嗯,PostgreSQL搭配PostGIS插件…一整套下来,居然发布成功了。

下面讲讲是怎么一步一步研究的吧

正文

一、下载安装PostgreSQL和PostGIS

网上找了篇文章,下载安装很简单,附上两个链接:

安装 PostgreSQL:https://www.runoob.com/postgresql/windows-install-postgresql.html

安装PostGIS:https://blog.csdn.net/antma/article/details/83580859

安装成功后,进入 pgAdmin 可以看到已初始化了一个空间数据库,Schemas 下面比默认库多了几个,同时可以看到Casts(转换器)和Extensions(扩展)也比默认库多了很多,应该主要用于空间相关的处理:

image-20210610154119915.png

二、下载osm2pgsql

下载地址:customdebug.com/osm/osm2pgs…

下载后解压, 还需要准备一个文件:default.style

github.com/openstreetm…

注:新建一个txt文档,将以上链接内的代码拷贝进去,重命名-> default.style->将该文件放到...\osm2pgsql\x64文件夹下

image-20210610154527555.png

三、下载 osm数据

osm数据下载:www.openstreetmap.org/#map=12/31.…

image-20210610154857928.png
可以手动选择区域进行下载osm数据

四、修改PostgreSQL配置

修改 pg_hba.conf (路径:…\PostgreSQL\10\data)

将原 md5 改为 trust

wps2.jpg

五、OSM数据导入PostgreSQL中

1、新建一个数据库osm

image-20210610155127311.png

2、为数据库osm添加postgis对象和函数定义(postgis.sql)

image-20210610155329553.png

3、加载EPSG坐标系统定义(spatial_ref_sys.sql)

image-20210610155356836.png

4、导入osm数据

将下载的map.osm地图数据放到…\osm2pgsql\x64文件夹下,并在该位置打开指令窗口,(osm是新建的数据库,这个指令将下载的地图数据放入这个数据库)

image-20210610155448121.png

导入成功:

image-20210610155505130.png

5、预览

导入成功后,打开 pgAdmin 即可看到多了4张表:

image-20210610155549152.png

六、安装 GeoServer 并绑定数据

一、下载

下载地址:geoserver.org/release/sta…

wps3.jpg

Platform Independent Binary : 二进制版本,解压即可使用

二、安装

直接解压即可,如下图所示:

wps4.jpg

运行 bin 文件夹下的startup.bat (linux 为 startup.sh)

wps5.jpg

运行成功后,窗口不能关,这是服务:

wps6.jpg

打开 http://localhost:8080/geoserver/web/ 如下图,安装成功:

右上角登录,用户名:admin 密码:geoserver

wps7.jpg

三、绑定数据发布服务

1、添加工作区

选择“工作区” -> “添加新的工作区”:

image-20210610160644980.png

输入工作区名称和命名空间:

image-20210610160736229.png

2、添加新的数据存储

image-20210610160939976.png

新建数据源,选择 PostGIS - PostGIS Database:

image-20210610161021700.png

输入数据源的基本信息,工作区选择上面新建的工作区,连接参数填写 PostgreSQL的连接信息,database填写在上一篇中导入OSM数据的数据库:

image-20210610161125928.png

3、新建图层

数据源保存后,会自动进入新建图层,这里列出了此数据库中可发布的图层数据:

image-20210610161335399.png

点击发布,进入编辑图层:

image-20210610161502601.png

边框直接点“从数据中计算”,将会从图层中自动计算边框:

image-20210610161553430.png

保存后,再打开“图层” -> “添加新的资源”:

image-20210610161711628.png

参照上面发布图层,将四个图层都发布,我这里都已经发布成功了:

image-20210610161746782.png

4、添加图层组

image-20210610161900239.png

image-20210610161930795.png

将上面的4个图层添加进来:

image-20210610162138788.png

添加完图层后,点击“生在边界”,自动生成边界坐标:

image-20210610162347642.png

注意:图层是按顺序从第一层到最后一层加载的,第一层在最下面。

5、预览

打开 “Layer Preview” - “OpenLayers”:

image-20210610162516302.png

成功:

image-20210610162544733.png

总结:

这些只是将下载的地图的点、线、面等数据存储在数据库里面,后面还需要对地图进行样式的渲染,还有二次开发…任重而道远啊,加油!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

QT中类对象之间的通信尽量少使用信号槽

发表于 2021-06-10

大家都知道在QT中的信号槽是一种非常重要的方法,这种方法可以使不同的类对象之间进行高效的通信,而且这也是QT区别于其他框架的重要机制之一。这就像MFC和WIN32的消息机制一样。

但是我希望大家以后在使用QT时还是尽量少使用信号槽实现是对象之间的通信。除了一些必须的场合,比如按钮的点击事件、列表框的切换事件等。

为什么这样说呢?

一、在结构上来说,连接信号槽的地方、发射信号的地方、接受信号的地方往往不在一个地方或者离得很近的地方,有时不在一个模块中,这就给代码的阅读带来的很大的困难。当读到发射信号的地方,需要知道这个信号是发给谁的,同理阅读到槽函数时,也需要知道是哪个类对象发射出的哪个信号。当查看完信号槽连接的地方,弄明白发射和接受对象之后,继续在原来的地方阅读代码,往往会打断之前的思路,还有重新花时间来整理思路。就像线程之间的来回切换一下,每次切换都是需要资源成本的。

二、信号槽机制有点像C/C++中的goto句。第一次学C语言的时候,老师就说过,不要使用goto语句,会破坏代码的模块化,但是不明白为什么。随着经验的增加,发现模块与模块之间的高内聚和低耦合是多么的重要!而信号槽这种设计又非常的灵活,可以在不通过类、不用的模块之间发射和接受信号,这也就大大的破坏了模块之间的高内聚性。当开发的模块增多,开发的人员增加后,这种问题就会更加的凸显出来。

那么在QT中如果不使用信号槽在对象之间进行通信,该使用什么方法在不同的类对象之间进行通信呢?

下面看一个例子,先使用信号槽进行通信。
介绍:有两个界面:第一个界面和第二个界面,第一个界面有一个Label,第二个界面有一个编辑框。当点击第一个界面的按钮的时候,弹出第二个界面,当在第二个界面的编辑框编辑完成之后,点击按钮,编辑的内容就会在第一个界面的Label上显示出来。
在这里插入图片描述
代码如下:
第一个界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码#pragma once

#include <QtWidgets/QMainWindow>
#include "ui_SignalSlot.h"

class QtGuiSecond;

class SignalSlot : public QMainWindow {
Q_OBJECT

public:
SignalSlot(QWidget *parent = Q_NULLPTR);

private:
void init();

private slots:
void slotBtn();
void slotEditStr(QString str);
private:
Ui::SignalSlotClass ui;

QtGuiSecond* _guiSecond = nullptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码#include "SignalSlot.h"
#include "QtGuiSecond.h"

SignalSlot::SignalSlot(QWidget *parent)
: QMainWindow(parent) {
ui.setupUi(this);
init();
}

void SignalSlot::init() {
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slotBtn()));

_guiSecond = new QtGuiSecond;

connect(_guiSecond, SIGNAL(sigEditStr(QString)), this, SLOT(slotEditStr(QString)));
}

void SignalSlot::slotBtn() {
_guiSecond->show();
}

void SignalSlot::slotEditStr(QString str) {
ui.label->setText(str);
}

第二个界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cpp复制代码#pragma once

#include <QWidget>
#include "ui_QtGuiSecond.h"

class QtGuiSecond : public QWidget {
Q_OBJECT

public:
QtGuiSecond(QWidget *parent = Q_NULLPTR);
~QtGuiSecond();

private:
void init();

private slots:
void slotOK();
signals:
void sigEditStr(QString);

private:
Ui::QtGuiSecond ui;


};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cpp复制代码#include "QtGuiSecond.h"

QtGuiSecond::QtGuiSecond(QWidget *parent)
: QWidget(parent) {
ui.setupUi(this);
init();
}

QtGuiSecond::~QtGuiSecond() {
}

void QtGuiSecond::init() {
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slotOK()));
}

void QtGuiSecond::slotOK() {
QString str = ui.lineEdit->text();
emit sigEditStr(str);
}

可以看到第二个界面编辑的内容是通过信号槽的方式传过去了。

如果不使用信号槽该怎么传递数据呢?

这就需要我们添加第三个类,也就是接口类,这个类的目的就是为了传递参数的。

1
2
3
4
5
6
7
8
9
10
cpp复制代码#pragma once
#include <QString>

class IEditArg {
public:
IEditArg();
~IEditArg();

virtual void editStr(QString str);
};
1
2
3
4
5
6
7
8
9
10
11
cpp复制代码#include "IEditArg.h"

IEditArg::IEditArg() {
}

IEditArg::~IEditArg() {
}

void IEditArg::editStr(QString str) {

}

然后让第一个界面类继承这个类,并且实现里面的虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cpp复制代码#pragma once

#include <QtWidgets/QMainWindow>
#include "ui_SignalSlot.h"
#include "IEditArg.h"

class QtGuiSecond;

class SignalSlot : public QMainWindow, public IEditArg {
Q_OBJECT

public:
SignalSlot(QWidget *parent = Q_NULLPTR);

void editStr(QString str)override;
private:
void init();

private slots:
void slotBtn();
private:
Ui::SignalSlotClass ui;

QtGuiSecond* _guiSecond = nullptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码#include "SignalSlot.h"
#include "QtGuiSecond.h"

SignalSlot::SignalSlot(QWidget *parent)
: QMainWindow(parent) {
ui.setupUi(this);
init();
}

void SignalSlot::editStr(QString str) {
ui.label->setText(str);
}

void SignalSlot::init() {
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slotBtn()));

_guiSecond = new QtGuiSecond;

_guiSecond->setArgInterface(this);
}

void SignalSlot::slotBtn() {
_guiSecond->show();
}

第二个界面类保存这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cpp复制代码#pragma once

#include <QWidget>
#include "ui_QtGuiSecond.h"

class IEditArg;

class QtGuiSecond : public QWidget {
Q_OBJECT

public:
QtGuiSecond(QWidget *parent = Q_NULLPTR);
~QtGuiSecond();

void setArgInterface(IEditArg* editArg);
private:
void init();

private slots:
void slotOK();

private:
Ui::QtGuiSecond ui;

IEditArg* _editArg = nullptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cpp复制代码#include "QtGuiSecond.h"
#include "IEditArg.h"

QtGuiSecond::QtGuiSecond(QWidget *parent)
: QWidget(parent) {
ui.setupUi(this);
init();
}

QtGuiSecond::~QtGuiSecond() {
}

void QtGuiSecond::setArgInterface(IEditArg* editArg) {
_editArg = editArg;
}

void QtGuiSecond::init() {
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(slotOK()));
}

void QtGuiSecond::slotOK() {
QString str = ui.lineEdit->text();
if (_editArg!=nullptr){
_editArg->editStr(str);
}
}

这样就完成了,虽然代码多了一些但是结构更清晰了!同样也是实现相同的功能呢!有利用了C++的特性。

aaa

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

看完这个还不了解redis的SDS,半夜你来扒我家墙头😄

发表于 2021-06-10

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

概述:

我们都知道redis底层是用C语言实现的,而C的字符串是以‘\0’来判断结束的,是一个对特殊字符存储不安全的。那redis是怎么做到存储二进制安全的呢?

重头戏-redis中的字符串

redis中还是有用到C中字符串的地方,比如一些不需要修改的字面量一些reids要输出的日志信息等。而需要修改的字符串redis抽象成了我们的SDS对象:简单动态字符串(simple dynamic string,SDS)。这部分的源码对应在这两个文件中

sds.h

和 sds.c

SDS的定义

每个 sds.h/sdshdr 结构表示一个 SDS 值,结构如下:

1
2
3
4
5
6
7
8
9
10
11
c复制代码struct sdshdr {

// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串,
char buf[];

};

我们的buf中存放的就是我们真正的按照\0结束的字符串内容(在3.2版本之后此处改成了一个指针会根据不同长度指向不同的结构),len是我们的字符串内容的长度。还可以看到有一个free的子段,这个子段的作用先卖个关子下边说😄

SDS中free的作用

在c语言中使用不当经常会有内存溢出的问题,导致我们的字符串被覆盖,而redis是怎么避免这种问题的出现的呢?

首先我们在redis中插入一个key:

set demo hello;

这个时候的SDS对象是:

1
2
3
4
5
6
c复制代码struct sdshdr {
int len; = 5
int free; = 0
char buf[]; = 'hello'

};

set demo ‘hello world’;

这个时候的SDS对象是:

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码struct sdshdr {
int len; = 11
int free; = 11
char buf[]; = 'hello world'

};
> set demo 'hello world wang';
struct sdshdr {
int len; = 16
int free; = 6
char buf[]; = 'hello world wang'

};

我们在set hello world之后 redis会根据我们的free子段去判断需不需要向操作系统申请内存,如果free够用则不申请。这个时候就涉及到redis的内存分配策略了,为了避免多次向操作系统申请内存的开销,redis在每次申请都会按照当前len的两倍去申请(当len于1M时候就只会多申请1M),也就可以解释我们的free为什么是11了,这就是redis的空间预分配。

我们可以看下redis-5源码涉及到内存分配的策略,扩容是这个方法XDM可以去看看 sds sdsMakeRoomFor(sds s, size_t addlen)

还有一个惰性空间释放的概念会涉及到我们的free子段,当我们将redis的字符串内容减少的时候,redis并不会立马释放内存而是仅仅修改free子段,以便下次使用。

总结

  • 因为SDS有存储字符串长度len,所以我们获取redis字符串长度是一个O(1)的操作。
  • len的存在避免了内存溢出的问题
  • 通过内存分配策略,减少了字符串修改时候的内存分配次数

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RocketMQ-延迟消息

发表于 2021-06-10

这一讲,我们主要来讲延迟消息。

这一次我们结合业务来讲。

业务背景

在电商中,下单后,有一些用户付款后,会主动退款。也有一些用户下单后,未付款。但是这部分未付款的订单,会占用着商品库存。

我们目前的电商App,下单后,会在订单表创建对应的订单数据。这些订单的状态,有一些是未付款的,但是未付款的订单占用着商品库存。为了让商品库存能正常恢复,我们现在的处理方案是:

  • 启动一个定时任务,每30分钟,定时扫描一遍订单表
  • 如果订单是已付款,则跳过,不处理
  • 如果订单是未付款,但未超过30分钟,不处理
  • 如果订单是未付款,且超过30分钟,就取消订单
    (补充:取消订单,其实就是下单的逆向流程)

方案缺点

这个方案有什么缺点?

  • 第一,每次定时任务去扫描全部订单,但是订单未付款且超时30分钟的只有一小部分。就是做很多无用功。
  • 第二,如果订单表的数量超级超级大,这个时候,扫描的时间巨长,浪费cpu资源。
  • 第三,这样子频繁查询数据库,给数据库造成很多不必要的压力。

解决方案

那针对上述的缺点,我们有没有好的解决方案:

  • 第一,避免扫描全表
  • 第二,谁没付款,就去取消谁,不要做多余的动作
  • 第三,要保证近实时取消订单。(近实时:1s左右)

说了这么多,我摊牌了,不装了,就是为了引入RocketMQ的延迟消息

简单总结一下:创建订单的时候,发送一条延时30分钟的消息。到30分钟后,消费者拿到信息,再去判断订单是否已付款,如果付款就跳过不处理,没付款,那就取消订单。

这种方案:没有多余的扫描数据库操作;谁没付款,就去取消谁。多好呀!在生产上,赶紧用起来。

生产者

上面,介绍的都是方法论,下面就是具体的实操环节了。

下面,简单用一个demo介绍一下生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Producer {
public static void main(String[] args) throws Exception{
//生产者组
DefaultMQProducer producer = new DefaultMQProducer("delay_producer_group");

//设置nameserver
producer.setNamesrvAddr("localhost:9876");
//启动生产者
producer.start();

//构建消息
Message message = new Message("delayTopic","TagA","delayMessage".getBytes(RemotingHelper.DEFAULT_CHARSET));
// 重点:设置延迟级别
message.setDelayTimeLevel(3);
// 发送消息
SendResult sendResult = producer.send(message);
// 打印发送结果
System.out.println("发送结果:"+sendResult);
// 关闭生产者
producer.shutdown();
}
}

这里强调一下,不是延迟发送哈,是延迟消费。发送是立马就发送的,只是消费的时候,延迟30分钟。

补充知识点

延迟级别是从1开始的,不是从0开始。然后你可能会发现,最多延迟2小时。如果你想延迟3小时,对不起,RocketMQ不支持。告辞!!!

消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public class Consumer {
public static void main(String[] args) throws Exception {
// 消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay_consumer_group");
//注册nameserver
consumer.setNamesrvAddr("localhost:9876");

// 订阅主题
consumer.subscribe("delayTopic","TagA");

// 开启消费offset
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);


//监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (int i = 0; i < list.size(); i++) {
MessageExt messageExt = list.get(i);
String msg = new String(messageExt.getBody());
//这里主要打印延迟时间≈当前时间-消息的生产时间
System.out.println(msg+" 时间="+(System.currentTimeMillis()-messageExt.getBornTimestamp()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

consumer.start();
}
}

总结:延迟消费者和普通的消费者相同,一毛一样。延迟消息的核心点:生产者多了一个延迟级别。

知其然知其所以然

上面,你已经知道怎么使用了。

如果面试官问你:RocketMQ的延迟消息底层原理是什么?

那你接着看下去。

看图说话。

  • 第一,生产者发送的消息,因为带了延迟级别,因此会被分发到叫SCHEDULE_TOPIC_XXXX的Topic中。里面有18的队列,一个队列对应着一个延迟级别。比如queueId=delayLevel-1。
  • 第二,定时器,每100毫秒,扫描所有延迟级别里面的延迟消息message,如果消费时间已经大于当前时间,那定时器就会把延迟消息message,发送到真正的topic(就是代码写的topic,比如上面代码的:delayTopic),根据负载均衡策略,把message发送到具体某个队列。
  • 第三,有消息后,消费者进行消息和后续处理。

上面这里,是一个总体流程图。

然后,我们对照代码,来进一步深刻认识一下。其实,就是加深理解。

第一步:生产者发送的消息到SCHEDULE_TOPIC_XXXX的topic

org.apache.rocketmq.store.CommitLog#putMessage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ini复制代码        //真正的topic
String topic = msg.getTopic();
//真正的队列Id
int queueId = msg.getQueueId();

final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
// 延迟级别大于0
if (msg.getDelayTimeLevel() > 0) {
// 如果延迟级别大于最大延迟级别,那就把延迟级别设置为最大延迟级别
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
// 延迟topicSCHEDULE_TOPIC_XXXX
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
// 根据延迟级别,获取队列id
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

// Backup real topic, queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
// 消息的topic设置为延迟topic,不是设置真正的topic
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}

省略部分封装msg的代码..
//最后把msg追加到mappedFile上,mappedFile这个后续再讲,在这里你把它当做一个文件即可
result = mappedFile.appendMessage(msg, this.appendMessageCallback);

第二步:定时器扫描信息

  • 1,org.apache.rocketmq.store.schedule.ScheduleMessageService#start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
kotlin复制代码public void start() {
//通过AtomicBoolean 来确保 有且仅有一次执行start方法
if (started.compareAndSet(false, true)) {
this.timer = new Timer("ScheduleMessageTimerThread", true);
// 遍历所有 延迟级别
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
// key为延迟级别
Integer level = entry.getKey();
// value 为 毫秒数
Long timeDelay = entry.getValue();
// 根据延迟级别 ,获取对应的offset
Long offset = this.offsetTable.get(level);
//
if (null == offset) {
offset = 0L;
}
// 为每个延迟级别创建定时任务,开始执行定时任务,1S后开始执行
if (timeDelay != null) {
// 第二步:具体核心执行逻辑在DeliverDelayedMessageTimerTask-->executeOnTimeup()
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
}
// 延迟10秒后执行定时任务,flushDelayOffsetInterval=10s,周期也是10秒执行一次
this.timer.scheduleAtFixedRate(new TimerTask() {

@Override
public void run() {
try {
//持久化每个队列的消费offset
if (started.get()) ScheduleMessageService.this.persist();
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}
}

2,org.apache.rocketmq.store.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask#executeOnTimeup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
ini复制代码public void executeOnTimeup() {
//根据延迟级别和topic:RMQ_SYS_SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX";来找到对应的ConsumeQueue
ConsumeQueue cq =
ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));
// 消费偏移量
long failScheduleOffset = offset;

if (cq != null) {
// 根据消费偏移量从消息队列中获取所有有效消息
SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
if (bufferCQ != null) {
try {
long nextOffset = offset;
int i = 0;
ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
// 遍历所有消息
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
// 获取消息的物理偏移量
long offsetPy = bufferCQ.getByteBuffer().getLong();
// 获取消息的物理长度
int sizePy = bufferCQ.getByteBuffer().getInt();
long tagsCode = bufferCQ.getByteBuffer().getLong();


//当前时间
long now = System.currentTimeMillis();
//消费时间
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
//下一个偏移量
nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
//如果消费时间<当前时间,说明应该被消费了
long countdown = deliverTimestamp - now;

if (countdown <= 0) {
//根据物理偏移量和长度,获取消息
MessageExt msgExt =
ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
offsetPy, sizePy);

if (msgExt != null) {
try {
//构建真正 的消息
MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);

// 重新把消息发送到真正的消息队列上
PutMessageResult putMessageResult =
ScheduleMessageService.this.writeMessageStore
.putMessage(msgInner);
...省略一堆不太重要的代码
}
//这里又重新添加一个新的任务,这次是100毫秒
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
failScheduleOffset), DELAY_FOR_A_WHILE);
}

第三步: 消费者后续处理(略)

最后用一张图来总结

好了,写完了,下期见,拜拜。

有问题的话,欢迎留言交流。

每日一问

RocketMQ不支持自定义延迟时间,那Kafka支持延迟消息吗?如果支持,支持自定义延迟时间吗?如要你实现自定义延迟时间,你会怎么实现?说说你的思路

欢迎留言

后续文章

  • RocketMQ-入门(已更新)
  • RocketMQ-架构和角色(已更新)
  • RocketMQ-消息发送(已更新)
  • RocketMQ-消费信息
  • RocketMQ-消费者的广播模式和集群模式(已更新)
  • RocketMQ-顺序消息(已更新)
  • RocketMQ-延迟消息(已更新)
  • RocketMQ-批量消息
  • RocketMQ-过滤消息
  • RocketMQ-事务消息
  • RocketMQ-消息存储
  • RocketMQ-高可用
  • RocketMQ-高性能
  • RocketMQ-主从复制
  • RocketMQ-刷盘机制
  • RocketMQ-幂等性
  • RocketMQ-消息重试
  • RocketMQ-死信队列

…

欢迎各位入(guan)股(zhu),后续文章干货多多。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

王者并发课-青铜10:千锤百炼-如何解决生产者与消费者经典问

发表于 2021-06-10

欢迎来到《王者并发课》,本文是该系列文章中的第10篇。

在本篇文章中,我将为你介绍并发中的经典问题-生产者与消费者问题,并基于前面系列文章的知识点,通过wait、notify实现这一问题的简版方案。

一、生产者与消费者问题

生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多进程、线程同步问题的经典案例。

这个问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。

生产者与消费者问题的关键在于要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。

同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用线程间通信的方法解决该问题,常用的方法有信号量等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。

当然,生产者与消费者问题并不是局限于单个生产者与消费者,在实际工作中,遇到更多的是多个生产者和消费者的情形。

生产者与消费者模式在软件开发与设计中有着非常广泛的应用。在这一模式中,生产者与消费者相互独立,它们仅通过缓冲区传递数据,因此可以用于程序间的解耦、异步削峰等。

生产者与消费者问题的要点:

  • 生产者与消费者解耦,两者通过缓冲区传递数据;
  • 缓冲区数据装满了之后,生产者停止数据生产或丢弃数据;
  • 缓冲区数据为空后,消费者停止消费并进入等待状态,等待生产者通知。

二、实现生产者与消费者方案

本节中,我们通过王者中的一个场景来模拟生产者与消费者问题。

在王者中,英雄兰陵王需要通过打野来发育,但是野区的野怪在被打完之后,需要隔一段时间再投放。

所以,我们创建两个线程,一个作为生产者向野区投放野怪,一个作为消费者打怪。

生产者:每秒检查一次野区,如果野区没有野怪,则进行投放。野怪投放后,通知打野英雄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// 野怪投放【生产者】
public static class WildMonsterProducer implements Runnable {
public void run() {
try {
createWildMonster();
} catch (InterruptedException e) {
System.out.println("野怪投放被中断");
}
}

//投放野怪,每1秒检查一次
public void createWildMonster() throws InterruptedException {
for (int i = 0;; i++) {
synchronized(wildMonsterArea) {
if (wildMonsterArea.size() == 0) {
wildMonsterArea.add("野怪" + i);
System.out.println(wildMonsterArea.getLast());
wildMonsterArea.notify();
}
}
Thread.sleep(1000);
}
}
}

消费者:打野英雄兰陵王作为消费者,在野区打怪发育。如果野区有野怪,则打掉野怪。 如果没有,会进行等待野区新的野怪产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// 兰陵王,打野英雄
public static class LanLingWang implements Runnable {
public void run() {
try {
attackWildMonster();
} catch (InterruptedException e) {
System.out.println("兰陵王打野被中断");
}
}

// 打野,如果没有则进行等待
public void attackWildMonster() throws InterruptedException {
while (true) {
synchronized(wildMonsterArea) {
if (wildMonsterArea.size() == 0) {
wildMonsterArea.wait();
}
String wildMonster = wildMonsterArea.getLast();
wildMonsterArea.remove(wildMonster);
System.out.println("收获野怪:" + wildMonster);
}
}
}
}

创建野区,并启动生产者与消费者线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class ProducerConsumerProblemDemo {

// 野怪活动的野区
private static final LinkedList<String> wildMonsterArea = new LinkedList<String>();

public static void main(String[] args) {
Thread wildMonsterProducerThread = new Thread(new WildMonsterProducer());
Thread lanLingWangThread = new Thread(new LanLingWang());

wildMonsterProducerThread.start();
lanLingWangThread.start();
}
}

在上面几段代码中,你需要重点注意的是synchronized、wait和notify用法,它们是本次方案的关键。运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码野怪0
收获野怪:野怪0
野怪1
收获野怪:野怪1
野怪2
收获野怪:野怪2
野怪3
收获野怪:野怪3
野怪4
收获野怪:野怪4
野怪5
收获野怪:野怪5
野怪6
收获野怪:野怪6

从结果可以看到,生产者在创建野怪后,打野英雄兰陵王会进行打野,实现了生产者与消费者的问题。

小结

以上就是关于线程异常处理的全部内容,在本文中我们基于wait、notify来解决生产者与消费者问题。对于本文内容,你需要理解生产者与消费者问题的核心是什么。另外,本文所提供的方案仅仅是这一问题多种解决方案中的一种,在后面的文章中,我们会根据新的知识点提供其他的解法。

正文到此结束,恭喜你又上了一颗星✨

夫子的试炼

  • 编写代码实现生产者与消费者问题。

延伸阅读与参考资料

  • Producer–consumer problem
  • 《王者并发课》专栏文集下载:github.com/ThoughtsBet…

关于作者

专注高并发领域创作。姊妹篇小册《高并发秒杀的设计精要与实现》作者,关注公众号【MetaThoughts】,及时获取文章更新和文稿。


如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

漫谈 JAR JAR 快查手册

发表于 2021-06-09

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

总文档 :文章目录

Github : github.com/black-ant

每次文章都很尽量走纯技术路线 ,为什么就没人点赞呢?????

一 . 前言

本篇文章梳理一下一个 JAR 包 , 有什么涉及的要点

1.1 JAR 简述

JAR文件 是一种基于Zip的文件格式,用于将许多文件汇总到一个 , JAR 文件本质上是一个 zip 文件,其中包含一个可选的 META-INF 目录。

JAR 命令 是基于 ZIP 和 ZLIB 压缩格式的通用归档和压缩工具。最初,jar 命令用于打包 Java applet (自 JDK 11以来不受支持)或应用程序; 然而,从 JDK 9开始,用户可以使用 jar 命令创建模块化 jar

二 . 包结构

Java 里面想运行一个项目 , 通常有 war 和 jar 2种方式 , 过去最常见的就是通过 war 包部署项目 ,

JAR_System.jpg


包结构作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码.
|-- BOOT-INF
| |-- classes
| | `-- com
| | `-- gang
| | `-- study
| | `-- maven
| | |-- BootdependencyApplication.class
| | `-- service
| | `-- AopAdvice.class
| `-- lib
| |-- 省略 JAR
|-- META-INF
| |-- MANIFEST.MF
| `-- maven
| `-- com.gang.study.maven.bootdependency
| `-- com-gang-study-maven-bootdependency
| |-- pom.properties
| `-- pom.xml
`-- org
`-- springframework
`-- boot
`-- loader
|-- 省略 class

来看一下正常包结构中各个目录的作用

// META-INF 目录
用于存储包和扩展配置数据,包括安全性、版本控制、扩展、配置应用程序、类加载器和服务 , META-INF 目录中的文件/目录由java2平台识别和解释

  • MANIFEST.MF : 用于定义扩展和包相关数据的清单文件
  • INDEX.LIST : 由 “-i” 命令生成 , 该文件包含应用程序或扩展中定义的包的位置信息。它是 JarIndex 实现的一部分,由类装入器用来加速其类装入过程
  • x.SF : JAR 文件的签名文件. ‘ x’代表基本文件名
  • x.DSA : 具有相同基文件名的签名文件关联的签名块文件。此文件存储相应签名文件的数字签名
  • maven : maven 的配置消息

BOOT-INF 目录 在后文详述

附录 一 : MANIFEST.MF 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码// 规范的版本
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
// 多重发布 : 存在多个 JDK 版本
Multi-Release: true
//创建清单文件的用户的名称
Built-By: 10169
// SpringBoot 启动类
Start-Class: com.gang.study.maven.BootdependencyApplication
// SpringBoot 类路径
Spring-Boot-Classes: BOOT-INF/classes/
// Spring-Boot lib 文件路径
Spring-Boot-Lib: BOOT-INF/lib/
// Spring Boot 版本
Spring-Boot-Version: 2.2.6.RELEASE
// 创建清单文件的工具版本和供应商
Created-By: Apache Maven 3.6.1
// 此自定义头给出创建清单文件的用户的名称
Build-Jdk: 1.8.0_152
// Main 方法入口 , 这里更像一个加载器
Main-Class: org.springframework.boot.loader.JarLauncher
// 到库或资源的相对路径的空格分隔列表
Class-Path: core.jar lib/ properties/


//---------------- 其他的属性
Name: 包名
Implementation-Build-Date: 实施的构建日期
Implementation-Title: 执行的标题
Implementation-Vendor: 实施的供应商
Implementation-Version: 实施版本
Specification-Title: 规范的标题
Specification-Vendor: 规范的供应商
Specification-Version: 规格版本
Sealed: 那么包的所有类是否都来自同一个 JAR


// 一 : 格式
MANIFEST.MF 是一个标准的 key:value ( headers:attributes)
key1: value1
Key2: value2
// 有效的标题必须在冒号和值之间有空格。另一个重要的地方是在文件的末尾必须有一个新的行 (这个地方坑了很久 , 怎么都导不进去 , 需要换行才对) , 否则最后一行会被忽略

附录二 : 包签名

JavaTM 平台允许您对 JAR 文件进行数字签名 , 在对 JAR 文件进行签名时,还可以选择对签名进行时间戳 , 时间戳可用于验证用于签名 JAR 文件的证书在签名时是否有效.

为此 , 还可以配置安全策略控制 , 可以将策略配置为向 applet 和应用程序授予安全特权 ,例如,您可以授予 applet 执行通常禁止的操作的权限,例如读写本地文件或运行本地可执行程序

一个签名主要由 4个元素完成 : 私钥 , 公钥与证书 , 摘要值

签名的使用方式 : Java 平台通过使用称为公钥和私钥的特殊号码来实现签名和验证

使用流程 : 签名者使用私钥为 JAR 文件签名 , 应用的公钥与证书一起放在 JAR 文件中,以便任何想要验证签名的人都可以使用它

摘要值 : 摘要值是文件内容的散列或编码表示形式,与签名时相同。当且仅当文件本身发生变化时,文件摘要才会发生变化。

证书 : 仅使用公钥和私钥还不足以真正验证签名 , 需要某种方法来确认公钥实际上来自它声称来自的签名者 , 这就用到了证书的概念 (添加一个附加元素 , 该元素是签名者在签名 JAR 文件中包含的证书)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JAVA复制代码
// 当一个 JAR 文件被签名时,一个签名文件被自动生成并放置在 JAR 文件的 META-INF 目录中
// 这个目录包含了归档文件的清单 , 签名文件的文件名是.SF 扩展

Signature-Version: 1.0
SHA1-Digest-Manifest: h1yS+K9T7DyHtZrtI+LxvgqaMYM=
Created-By: 1.7.0_06 (Oracle Corporation)

Name: test/classes/ClassOne.class
SHA1-Digest: fcav7ShIG6i86xPepmitOVo4vWY=

Name: test/classes/ClassTwo.class
SHA1-Digest: xrQem9snnPhLySDiZyclMlsFdtM=

Name: test/images/ImageOne.gif
SHA1-Digest: kdHbE7kL9ZHLgK7akHttYV4XIa0=

Name: test/images/ImageTwo.gif
SHA1-Digest: mF0D5zpk68R4oaxEqoS9Q7nhm60=

三 . 常用操作

3.1 获取 jar 包中类路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码// JAR 命令获取 
打印出 jar 中所有的class名 : jar tf com-gang-bootdependency-1.0-SNAPSHOT.jar
打印所有的 class : jar tf com-gang-bootdependency-1.0-SNAPSHOT.jar | grep '\.class$'

// Java 代码获取
public static Set<String> getClassNamesFromJarFile(File givenFile) throws IOException {
Set<String> classNames = new HashSet<>();
try (JarFile jarFile = new JarFile(givenFile)) {
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry jarEntry = e.nextElement();
if (jarEntry.getName().endsWith(".class")) {
String className = jarEntry.getName()
.replace("/", ".")
.replace(".class", "");
classNames.add(className);
}
}
return classNames;
}
}

// 运行时动态获取
public static Set<Class> getClassesFromJarFile(File jarFile) throws IOException, ClassNotFoundException {
Set<String> classNames = getClassNamesFromJarFile(jarFile);
Set<Class> classes = new HashSet<>(classNames.size());
try (URLClassLoader cl = URLClassLoader.newInstance(
new URL[] { new URL("jar:file:" + jarFile + "!/") })) {
for (String name : classNames) {
Class clazz = cl.loadClass(name); // Load the class by its name
classes.add(clazz);
}
}
return classes;
}

3.2 生成tree树结构的几种方式

如果想要打印 tree 结构有以下几种方式 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码
// 方式一 : tree 命令可以用于 Windows 生成目录数 (PS : 应该是系统本身就有该命令)
打印到当前文件 : tree /f >tree.txt


// 方式二 : tree for windows (PS : 这个和上面不是一个东西)
1. 下载 tree for windows 文件 : http://gnuwin32.sourceforge.net/packages/tree.htm (选择 Binaries.zip)
2. 解压获取 tree.exe , 放在 git 项目中 : Git\usr\bin (注意 , 外层的 bin , 需要到 usr 目录下)
3. 项目路径下反键 - git base here (即 git 控制台)
4. 运行 tree 命令

-A 使用ASNI绘图字符显示树状图而非以ASCII字符组合。
-C 在文件和目录清单加上色彩,便于区分各种类型。
-d 显示目录名称而非内容。
-D 列出文件或目录的更改时间。
-f 在每个文件或目录之前,显示完整的相对路径名称。
-F 在执行文件,目录,Socket,符号连接,管道名称名称,各自加上"*","/","=","@","|“号。
-g 列出文件或目录的所属群组名称,没有对应的名称时,则显示群组识别码。
-i 不以阶梯状列出文件或目录名称。
-I 不显示符合范本样式的文件或目录名称。
-l 如遇到性质为符号连接的目录,直接列出该连接所指向的原始目录。
-n 不在文件和目录清单加上色彩。
-N 直接列出文件和目录名称,包括控制字符。
-p 列出权限标示。
-P 只显示符合范本样式的文件或目录名称。
-q 用”?"号取代控制字符,列出文件和目录名称。
-s 列出文件或目录大小。
-t 用文件和目录的更改时间排序。
-u 列出文件或目录的拥有者名称,没有对应的名称时,则显示用户识别码。
-x 将范围局限在现行的文件系统中,若指定目录下的某些子目录,其存放于另一个文件系统上,则将该子目录予以排除在寻找范围外。


$ tree
.
|-- classes
| `-- com
| `-- gang
| `-- study


// 方式三 : 在线 tree 转换
http://dir.yardtea.cc/

// 方式四 : 使用软件显示如上图所示的数结构
下载软件 DirPrintOK

3.3 JAR 反编译方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// 方案一 :  工具查看

1. ZIP 解压后 , 直接通过 Java Decompiler 查看
?- 该方案对于部分 class 有局限性 , 看出来的如下所示 : // INTERNAL ERROR //
2. JD Project : 最常用的最好的 java 脱机反编译器之一
3. Cavaj Java Decompiler
4. DJ Java Decompiler
5. JBVD
6. androidchef
7. Procyon
8. CFR Decompiler
9. FernFlower
10. Krakatau
11. Luyten

// 编译器大同小异 , 无非就是转码 , 以上任意一种都可以试着用一下 , 一般我自己使用 JD-GUI / Jar Explorer
https://java-decompiler.github.io/


// 方案二 : 命令行反编译
java -jar JDCommandLine.jar ${TARGET_JAR_NAME}.jar ./classes

3.4 如何预防反编译问题

Java 字节码保留有关字段、方法返回值和参数的类型信息,但是它不包含本地变量的类型信息。

Java 类文件中的类型信息使得字节码的反编译任务比机器代码的反编译更加容易。

因此,反编译 Java 字节码需要分析大多数局部变量类型,平坦堆栈指令和结构化循环和条件。

方案一 : 使用 YGuard 混淆类文件 (类似的还有 ProGuard 等 )

可以将已编译的代码转换为人类难以理解的代码 , 同时有助于减少应用程序的启动时间.

最重要的是 , 这是一个完全开源的程序. YGuard 的混淆方式 :

  • 通过使用不可表达的字符替换包、类、方法和字段名,从逆向工程中删除类文件
  • 收缩引擎分析所有输入 Jar 文件的字节码,以确定从一组给定的代码入口点无法到达哪些代码实体。然后 yGuard 将删除这些过时的代码片段(整个类或单个方法和字段)。

YGuard 模糊的是 class 文件 , YGuard 是一个免费的开源 Java 字节码混淆器和 shrinker , 通过模糊处理,参考地址 :

@ github.com/yWorks/yGua…

@ github.com/yWorks/yGua…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码<dependency>
<groupId>com.yworks</groupId>
<artifactId>yguard</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>

<!-- 配置规则详见官方文档 -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<id>obfuscate</id>
<configuration>
<tasks>
<property name="runtime_classpath" refid="maven.runtime.classpath"/>
<taskdef name="yguard" classname="com.yworks.yguard.YGuardTask" />
<yguard>
<!-- see the yGuard task documentation for information about the <yguard> element-->
</yguard>
</tasks>
</configuration>
</execution>
</executions>
</plugin>

方案二 : 如果想要隐藏某个字符串 , 可以考虑对属性加密

方案三 :很多反编译器没有适应新特性和某些工具框架 . 例如 : lambdas

方案四 :定制一个 ClassLoader 用于专门的解密

方案五 : 使用 protector4j 对 jar 文件加密 , 将 JAR 文件转换为私有的 JARX 格式,保护类文件和应用程序结构
@ protector4j.com/

(PS : 个人没用过 , 不评价 , 类似的还有 JarProtector , 不过 protector4j 比较新 )

3.5 Maven 打包 JAR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
xml复制代码<!-- Maven pom.xml 文件构建 jar-->
<modelVersion>4.0.0</modelVersion>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<!-- Plugin Java-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
<mainClass>
com.baeldung.executable.ExecutableMavenJar
</mainClass>
</manifest>
</archive>
</configuration>
</plugin>


<!-- Plugin Spring -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>spring-boot</classifier>
<mainClass>
com.baeldung.executable.ExecutableMavenJar
</mainClass>
</configuration>
</execution>
</executions>
</plugin>

<!-- 通常也可以直接使用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

3.6 把文件打包到 JAR 中

可以使用 jar -u 命令更新 jar 中的文件 , 常常用于系统的定制等操作 , 通常的格式如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码

jar uf JarExample.jar com/baeldung/jar/JarExample.class


// 案例 : 需要替换 parent.jar 内部的 child-1.3.4.jar 依赖

// Step 1 : 准备好相关的路径 (例如需要放在该路径下 , 先在外部创建一个相同的路径)
构建目录 : \new_version\BOOT-INF\classes\bundles

// Step 2 : 将 bundles jar 移动到 该路径

// Step 3 : 执行命令执行
jar -uvf0 parent.jar BOOT-INF/classes/bundles/child-1.3.4.jar

四 . 其他操作

4.1 手动创建一个 JAR 文件

在这个案例里面 , 我会试着手动编译 .java 并且打包成一个 jar 文件并且执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码// Step 1 : 准备一个 .java 方法
public class DefaultMain {
public static void main(String[] args){
System.out.println("Test Default Main Success!");
}
}

// Step 2 : 打包为 .class 文件 (运行后生成一个 DefaultMain.class)
javac -d . DefaultMain.java


// Step 3 : (此处可以省略 , 参考后文设置清单文件)

// Step 4 : 打包到 JAR 文件中 (运行后生成一个 example.jar)
jar cfe example.jar com/gang/DefaultMain com/gang/DefaultMain.class

// Step 5 : 执行 jar
java -jar example.jar


// 附录 : JAR 包结构
.
|-- META-INF
| `-- MANIFEST.MF
|-- com
| `-- gang
| `-- DefaultMain.class
`-- tree.txt

3 directories, 3 files

4.2 Maven 添加 MANIFEST.MF 文件

这种方式是通过 Maven 编辑 MANIFEST.MF 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifest>
<packageName>com.baeldung.java</packageName>
</manifest>
<manifestEntries>
<Created-By>baeldung</Created-By>
</manifestEntries>
</archive>
</configuration>
</plugin>

4.3 JAR 添加 MANIFEST.MF 文件

通过 JAR 命令 手动添加 MANIFEST.MF , 其中最重要的一点 : 使用换行符结束清单文件 , 否者默认会忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// Step 1 : 准备一个任意的文本文件 , 添加属性(记得回车)
Built-By: gang

// Step 2 : 使用命令打包
jar cvfm example.jar manifest.txt com/gang/DefaultMain.class

../test>jar cvfm example.jar manifest.txt com/gang/DefaultMain.class
已添加清单
正在添加: com/gang/DefaultMain.class(输入 = 442) (输出 = 299)(压缩了 32%)

// 解压后最终结果 :
Manifest-Version: 1.0
Built-By: gang
Created-By: 1.8.0_152 (Oracle Corporation)

4.4 JAR 中定义 JDK 多版本

当期望 JAR 包能在多种版本的 JDK 运行时 , 可以使用如下方式构建 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
xml复制代码├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── gang
│ │ │ └── test
│ │ │ ├── DefaultVersion.java
│ │ │ └── App.java
│ │ └── java9
│ │ └── com
│ │ └── gang
│ │ └── test
│ │ └── DefaultVersion.java



<!-- Step 1 : 通过 Maven 构建 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java-8</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</execution>
<execution>
<id>compile-java-9</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>9</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
</compileSourceRoots>
<!-- > 3.7.1 时替换下句 : <multiReleaseOutput>true</multiReleaseOutput> -->
<outputDirectory>${project.build.outputDirectory}/META-INF/versions/9</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

<!-- Step 2 : MANIFEST 文件中将 Multi-Release 条目设置为 true -->
<!--使用这种配置,Java 运行时将在 JAR 文件的 META-INF/versions 文件夹中查找特定于版本的类; 否则,只使用基类-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</plugin>

4.5 Fat JAR 打包

什么是 Fat jat ?

Fat jar 同样是一个 jar , 与普通 jar 的区别是 : 它包含来自所有库的类,您的项目依赖于这些库,当然还有当前项目的类.

以 Spring 的打包方式为例 : Spring 将应用程序代码打包到BOOT-INF.classes,将依赖包打包到BOOT-INF.lib目录
Spring 这种应该就是 Fat JAR “ 将 dependencies 的 jar 复制到主 jar,然后使用特殊的类加载器加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
xml复制代码 




<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>source-release-assembly</id>
<phase>none</phase>
</execution>
<execution>
<id>source-release-assembly-no-eclipse-libs</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<runOnlyAtExecutionRoot>true</runOnlyAtExecutionRoot>
<descriptors>
<descriptor>src/assemble/${sourceReleaseAssemblyDescriptor}.xml</descriptor>
</descriptors>
<tarLongFileMode>gnu</tarLongFileMode>
</configuration>
</execution>
</executions>
</plugin>


<properties>
<start-class>org.baeldung.boot.Application</start-class>
</properties>

五 . JAR 命令及常见操作

5.1 JAR 命令模板

jar {ctxu}[vfm0M] [jar-文件] [manifest-文件] [-C 目录] 文件名 ...

其中 {ctxu} 是 jar 命令的子命令,每次 jar 命令只能包含 ctxu 中的一个,它们分别表示

-c : 创建新的 JAR 文件包

-t : 列出 JAR 文件包的内容列表

-x : 展开 JAR 文件包的指定文件或者所有文件

-u : 更新已存在的 JAR 文件包 (添加文件到 JAR 文件包中)

[vfm0M] : 中的选项可以任选,也可以不选,它们是 jar 命令的选项参数

-v : 生成详细报告并打印到标准输出

-f : 指定 JAR 文件名,通常这个参数是必须的

-m : 指定需要包含的 MANIFEST 清单文件

-0 : 只存储,不压缩,这样产生的 JAR 文件包会比不用该参数产生的体积大,但速度更快

-M : 不产生所有项的清单(MANIFEST〕文件,此参数会忽略 -m 参数

[jar-文件] : 即需要生成、查看、更新或者解开的 JAR 文件包,它是 -f 参数的附属参数

[manifest-文件] : 即 MANIFEST 清单文件,它是 -m 参数的附属参数

[-C 目录] : 表示转到指定目录下去执行这个 jar 命令的操作。它相当于先使用 cd 命令转该目录下再执行不带 -C 参数的 jar 命令,它只能在创建和更新 JAR 文件包的时候可用。

文件名 … : 指定一个文件/目录列表,这些文件/目录就是要添加到 JAR 文件包中的文件/目录。如果指定了目录,那么 jar 命令打包的时候会自动把该目录中的所有文件和子目录打入包中。

5.2 JAR 常用命令

@ jar-The Java Archive Tool (oracle.com)

一 : JAR 命令设置 Class 主类

1
2
3
4
5
6
java复制代码jar cfe example.jar com.gang.DefaultMain com/gang/*.class

// 生成的 MANIFEST 文件
Manifest-Version: 1.0
Created-By: 1.8.0_152 (Oracle Corporation)
Main-Class: com.gang.DefaultMain

二 : 运行 jar

1
JAVA复制代码java -jar test.jar

三 : jar 内容管理

1
2
3
4
5
6
7
8
java复制代码// 提取 JAR 内容
jar xf jar-file [archived-file(s)]

// 查看 JAR 内容
jar tf jar-file

// 更新 JAR 内容
jar uf jar-file input-file(s)

四 : 清单管理

1
2
3
4
5
6
7
8
java复制代码// 命令行修改清单文件
jar cfm jar-file manifest-addition input-file(s)

// 设置默认入口
jar cfm MyJar.jar Manifest.txt MyPackage/*.class

// 添加 class
jar cfm MyJar.jar Manifest.txt MyPackage/*.class

其他检索

1
2
3
4
5
6
7
8
9
java复制代码// 指定主类 : 
> java -cp JarExample.jar com.baeldung.jar.JarExample
> java -cp JarExample.jar com/baeldung/jar/JarExample

// 列出 Jar 的内容 :
> jar tf JarExample.jar

// 查看清单文件
> unzip -p JarExample.jar META-INF/MANIFEST.MF

六 . 其他

6.1 WAR 与 JAR 的区别

JAR 包

简单地说,JAR 或 Java Archive 是一种包文件格式 , JAR 文件具有 .Jar 扩展名 , 可能包含库、资源和元数据文件 , 实际上,它是一个压缩文件,包含. 类文件的压缩版本以及编译的 Java 库和应用程序的资源

WAR 包

WAR 是 Web Application Archive 或 Web Application Resource 的缩写 , 这些归档文件具有 .War 扩展,用于打包 web 应用程序,这些应用程序可以部署在任何 Servlet/JSP 容器上。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// WAR 包格式
META-INF/
MANIFEST.MF
WEB-INF/
web.xml
jsp/
helloWorld.jsp
classes/
static/
templates/
application.properties
lib/
// *.jar files as libs

6.2 java.util.jar api 使用

官方 API 文档

6.3 对比 BOOT-INF

前面看到还有一个 BOOT-INF , 这个文件夹的目的又是什么 ?

参考地址

BOOT-INF 结构的特性 :

  • Spring Boot 应用程序加载表单 BOOT-INF 文件夹
  • 应用程序类应该放在嵌套的 BOOT-INF/classes 目录中
  • 依赖项应该放在一个嵌套的 BOOT-INF/lib 目录中

BOOT-INF 解决的问题 :

Java 没有提供任何加载嵌套 jar 文件的标准方法(即加载本身包含在 jar 中的 jar 文件)。当需要分发一个可以从命令行运行而不需要解压缩的自包含应用程序时 , 会出现问题

PS : 其实这里就是前文说的 FAT JAR

为此 , Spring 采用了如下的目录结构 , 将 应用程序类放在一个嵌套的 BOOT-INF/classes 目录中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
JAVA复制代码example.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-classes
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
+-dependency1.jar
+-dependency2.jar

// SpringBoot 对于 War 包的目录格式 (依赖项应该放在嵌套的 WEB-INF/lib 目录中) :
example.war
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-WEB-INF
+-classes
| +-com
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
| +-dependency1.jar
| +-dependency2.jar
+-lib-provided
+-servlet-api.jar
+-dependency3.jar

BOOT-INF 的索引

前面说了 , BOOT-INF 说到底是为了聚合包和应用程序 , 他有多种索引文件 :

  • Classpath Index (classpath.idx) : 提供了将 jars 添加到类路径的顺序
  • Layer Index (layers.idx) : 提供了一个 Layer 和 JAR 中应该包含的部分的列表
1
2
3
4
5
6
7
8
9
10
11
JAVA复制代码// classpath.idx
- "BOOT-INF/lib/dependency2.jar"
- "BOOT-INF/lib/dependency1.jar"

// layers.idx
- "dependencies":
- "BOOT-INF/lib/dependency1.jar"
- "BOOT-INF/lib/dependency2.jar"
- "application":
- "BOOT-INF/classes/"
- "META-INF/"

BOOT-INF 的加载

BOOT-INF 基于 org.springframework.boot.loader.jar 进行加载 , 该类允许从标准 jar 文件或嵌套的子 jar 数据中加载 jar 内容

首次加载时,每个 JarEntry 的位置映射到外部 jar 的物理文件偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
|| A.class ||| B.class | C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
^ ^ ^
0063 3452 3980

// 可以看到 :
1. 在 myapp.jar 的位置0063的/BOOT-INF/classes 中找到 A.class
2. 在 myapp.jar 中位于3452的位置找到来自嵌套 jar 的 B.class
3. C.class 位于3980的位置

有了这些信息,我们就可以通过寻找外部 jar 的适当部分来加载特定的嵌套条目 , 这就是 BOOT-INF 解决外部以来的主要方式

同时 , Spring 提供了 多个启动器装载资源 :

  • Springframework.boot.loader.Launcher
    • 该类是一个特殊的引导类,用作可执行 jar 的主入口点。它是 jar 文件中的实际 Main-Class,用于设置适当的 URLClassLoader 并最终调用 main ()方法
  • JarLauncher : 子启动器 , 在 BOOT-INF/lib/中查找
  • WarLauncher : 子启动器 , 在 WEB-INF/lib/和 WEB-INF/lib-proved/中查找
  • PropertiesLauncher : 子启动器 , 默认在应用程序归档中查找 BOOT-INF/lib/ (允许添加额外路径)
1
2
3
java复制代码// 配置方式 :
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.mycompany.project.MyApplication

总结

不知道怎么写总结了 , 本来就是一篇总结文档 , 所以没什么需要总结的..

只是想吐槽一下 , 一直以来都在走技术路线 , 但是深深的感觉长篇幅的技术文档 , 受众面很小. 虽然写文档本身还是为了服务自己 , 但是没人看确实影响心态 , 一度怀疑是不是总结的不好 , 以后自己用来回顾的时候会不会也看不懂了~~

参考

官方参考文档

  • JAR File Specification (oracle.com)
  • The Java™ Tutorials

其他参考文档

  • www.baeldung.com/jar-file-ge…
  • www.baeldung.com/java-view-j…
  • www.baeldung.com/java-view-j…
  • www.baeldung.com/install-loc…
  • www.baeldung.com/executable-…
  • www.baeldung.com/deployable-…
  • www.baeldung.com/java-jar-ex…
  • www.baeldung.com/java-create…
  • www.baeldung.com/java-jar-wa…
  • www.baeldung.com/java-jar-ma…
  • www.baeldung.com/maven-multi…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

王者并发课-青铜9:防患未然-如何处理线程中的异常

发表于 2021-06-09

欢迎来到《王者并发课》,本文是该系列文章中的第9篇。

在本篇文章中,我将为你介绍线程中异常的处理方式以及uncaughtExceptionHandler用法。

一、新线程中的异常去哪了

应用程序在执行过程中,难免会出现各种意外错误,如果我们没有对错误进行捕获处理,会直接影响应用的运行结果,甚至导致应用崩溃。而在应用异常处理中,多线程的异常处理是比较重要又容易犯错的地方。

接下来,我们通过一段代码模拟一种常见的多线程异常处理方式。

在下面的代码中,我们在主线程中创建了新线程nezhaThread,并期望在主线程中捕获新线程中抛出的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码 public static void main(String[] args) {
Thread neZhaThread = new Thread() {
public void run() {
throw new RuntimeException("我是哪吒,我被围攻了!");
}
};
// 尝试捕获线程抛出的异常
try {
neZhaThread.start();
} catch (Exception e) {
System.out.println("接收英雄异常:" + e.getMessage());
}
}

运行结果如下:

1
2
3
4
shell复制代码Exception in thread "Thread-0" java.lang.RuntimeException: 我是哪吒,我被围攻了!
at cn.tao.king.juc.execises1.ExceptionDemo$1.run(ExceptionDemo.java:7)

Process finished with exit code 0

对于多线程新手来说,可能并不能直接看出其中的不合理。然而,从运行的结果中可以看到,没有输出“接收英雄异常”关键字。也就是说,主线程并未能捕获新线程的异常。 那这是为什么呢?

理解这一现象,首先要从线程的本质出发。在Java中,每个线程所运行的都是独立运行的代码片段,如果我们没有主动提供线程间通信和协作的机制,那么它们彼此之间是隔离的。

换句话说,每个线程都要在自己的闭环内完成全部的任务处理,包括对异常的处理,如果出错了但你没有主动处理异常,那么它们会按照既定的流程自我了结。

二、多线程中的异常处理方式

1. 从主线程看异常的处理

为了理解多线程中的错误处理方式,我们先看常见的主线程是如何处理错误的,毕竟相对于多线程,单一的主线程更容易让人理解。

1
2
3
java复制代码 public static void main(String[] args) {
throw new NullPointerException();
}

很明显,上面这段代码将会抛出下面错误信息:

1
2
shell复制代码Exception in thread "main" java.lang.NullPointerException
at cn.tao.king.juc.execises1.ExceptionDemo.main(ExceptionDemo.java:21)

对于类似于空指针错误的堆栈信息,相信你一定并不陌生。在主线程中处理这样的异常很简单,通过编写try、catch代码块即可。但其实,除了这种方式外,我们还可以通过定义uncaughtExceptionHandler来处理主线程中的异常。

1
2
3
4
5
6
7
8
9
10
11
java复制代码 public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
throw new NullPointerException();
}

// 自定义错误处理
static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("出错了!线程名:" + t.getName() + ",错误信息:" + e.getMessage());
}
}

输出结果如下:

1
2
3
shell复制代码出错了!线程名:main,错误信息:null

Process finished with exit code 1

你看,我们已经地捕获了异常。然而,你可能会疑惑为什么Thread.UncaughtExceptionHandler可以自定义错误处理?说到这,就不得不提Java中的异常处理方式,如下图所示:

在Java中,我们经常可以看到空指针那样的错误的堆栈信息,然而这个堆栈实则是线程在出错的情况下 “不得已” 才输出来的。从图中我们可以看到:

  • 当线程出错时,首先会检查当前线程是否指定了错误处理器;
  • 如果当前线程没有指定错误处理器,则继续检查其所在的线程组是否指定(注意,前面我们已经说过,每个线程都是有线程组的);
  • 如果当前线程的线程组也没有指定,则继续检查其父线程是否指定;
  • 如果父线程同样没有指定错误处理器,则最后检查默认处理是否设置;
  • 如果默认处理器也没有设置,那么将不得不输出错误的堆栈信息。

2. 多线程间的异常处理

不要忘记,主线程也是线程,所以当你理解了主线程的错误处理方式后,你也就理解了子线程中的异常处理方式,它们和主线程是相同的。在主线程中,我们可以通过Thread.setDefaultUncaughtExceptionHandler来设置自定义异常处理器。而在新的子线程中,则可以通过线程对象直接指定异常处理器,比如我们给前面的neZhaThread线程设置异常处理器:

1
2
java复制代码neZhaThread.setName("哪吒");
neZhaThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

那么,设置处理器后的线程异常信息则输出如下:

1
2
3
shell复制代码出错了!线程名:哪吒,错误信息:我是哪吒,我被围攻了!

Process finished with exit code 0

你看,通过定义uncaughtExceptionHandler,我们已经捕获并处理了新线程抛出的异常。

3. 理解UncaughtExceptionHandler

从上面的代码中,相信你已经直观地理解UncaughtExceptionHandler用法。在Java中,UncaughtExceptionHandler用于处理线程突然异常终止的情况。当线程因某种原因抛出未处理的异常时,JVM虚拟机将会通过线程中的getUncaughtExceptionHandler查询该线程的错误处理器,并将该线程和异常信息作为参数传递过去。如果该线程没有指定错误处理器,将会按照上图所示的流程继续查找。

三、 定义uncaughtExceptionHandler的三个层面

1. 定义默认异常处理器

默认的错误处理器可以作为线程异常的兜底处理器,在线程和线程组未指定异常处理器时,可以使用默认的异常处理器。

1
java复制代码 Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

2. 自定义特定的异常处理器

如果某个线程需要特定的处理器时,通过线程对象指定异常处理器是个很不错的选择。当然,这种异常处理器不可以与其他线程共享。

1
java复制代码neZhaThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

3. 继承ThreadGroup

通过继承ThreadGroup并覆写uncaughtException可以重设当前线程组的异常处理器逻辑。不过要注意的是,覆写线程组的行为并不常见,使用时需要慎重。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MyThreadGroupDemo  extends ThreadGroup{
public MyThreadGroupDemo(String name) {
super(name);
}

@Override
public void uncaughtException(Thread t, Throwable e) {
// 在这里重写线程组的异常处理逻辑
System.out.println("出错了!线程名:" + t.getName() + ",错误信息:" + e.getMessage());
}
}

小结

以上就是关于线程异常处理的全部内容,在本文中我们介绍了多线程异常的处理方式以及uncaughtExceptionHandler的用法。对于多线程的异常处理应该记住:

  • 线程内部的异常应尽可能在其内部解决;
  • 如果主线程需要捕获子线程异常,不可以使用try、catch,而是要使用uncaughtExceptionHandler。当然,已经在子线程内部捕获的异常,主线程将无法捕获。

正文到此结束,恭喜你又上了一颗星✨

夫子的试炼

  • 编写代码了解并体验uncaughtExceptionHandler用法。

延伸阅读与参考资料

  • 《王者并发课》专栏文集下载:github.com/ThoughtsBet…

关于作者

专注高并发领域创作。姊妹篇小册《高并发秒杀的设计精要与实现》作者,关注公众号【MetaThoughts】,及时获取文章更新和文稿。


如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…646647648…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%