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

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


  • 首页

  • 归档

  • 搜索

块存储监控与服务压测调优利器-EBS Lens发布 EBS监

发表于 2021-11-26

简介:SLS团队联合EBS团队发布了EBS Lens,针对块存储提供数据分析、资源监控的功能,可以帮助用户获取云上块存储资源信息与性能监控数据、提升云上块存储资源的管理效率、高效分析业务波动与资源性能消耗情况。

EBS监控现状

块存储是阿里云为云服务器ECS提供的块设备产品,具有高性能和低时延的特点。临近双十一,在大促的时候,磁盘IO往往是运维的重点,如果磁盘被打爆了,那么关键的业务可能会停滞甚至崩溃。EBS监控目前存在几个问题

  1. 块存储提供的原生监控仅限制在单实例级别,只能查看单个云盘的性能监控,缺少对全局云盘状态的监控,如果云盘数量很多,那么云盘状态的监控就非常麻烦

  1. 通过SLS Logtail, Telegraf或者云监控agent等,可以实现对单个ECS实例所有云盘的状态的监控。但是这些监控方式都是侵入式的,agent的安装、监控大盘的维护、云盘实例监控精细化控制、以及跨ECS实例的监控,对于用户来说,都有很大的学习成本和代价
  2. 分析维度单一,以上场景下,对云盘的监控和分析还是基于云盘id的,而云盘资产本身的属性也包含很多信息。比如用户想看到一个自己所有云盘资产的大图、各个地域的云盘分布、各个云盘类型的比例等信息,都是很难做到的。

正是考虑到用户对EBS监控的使用有如上的痛点,SLS团队联合EBS团队发布了EBS Lens(Lens, 透镜的意思,取名为Lens意味着洞察云产品细微的变化),针对块存储提供数据分析、资源监控的功能,可以帮助用户获取云上块存储资源信息与性能监控数据、提升云上块存储资源的管理效率、高效分析业务波动与资源性能消耗情况。

EBS Lens产品特点

自动化数据采集

EBS Lens开启后,SLS会自动从用户的EBS资产中拉取云盘列表。进入APP后首先展示的就是接入管理页面,在这个页面,可以看到EBS云盘全局的一个管理视图,包含以下信息:

  • 展示当前接入的云盘总量、数据采集的云盘数量、云盘的地域和目标存储库的数量。
  • 展示EBS实例信息。例如,实例ID、标签、云盘种类、云盘类型、可用区信息、采集状态、采集操作等信息。如果用户在开启EBS Lens之后,对EBS云盘有新建、更新和删除操作的话,SLS会自动更新这里的云盘列表

采集配置

EBS云盘资产同步过来之后,需要用户开启云盘资产的监控数据采集。在这里,我们提供两种采集方式,一种是供用户进行精细化管理的手动采集,还有一种是在EBS云盘数量过多的情况下,方便用户全局管理的自动化采集。

手动采集

  • 支持对单个实例的采集状态进行管理
  • 考虑到EBS实例数量会比较多,这里支持在单个分页下面进行批量开启/批量关闭操作

自动化采集

当用户云盘有几百甚至上千个的时候,手动采集的管理方式显然不能满足需求,因此我们还提供了一个自动化采集的功能。自动化采集提供了图形化的配置界面:

  • 可以使用地域、实例ID、付费类型、磁盘类型、标签等属性设置采集条件。
  • 标准模式下各个条件之间为且关系。高级模式下,您可以灵活组合与嵌套条件。

配置保存之后,自动化采集立刻开启。所有满足条件的云盘,都会自动打开日志采集,从而省去了手动操作的步骤,另外当实例有所增减的时候,自动化采集也能够感知实例的变化,进行相应的调整。

存储库信息展示

开启云盘监控数据采集之后,SLS会从EBS云盘上拉取监控数据,然后投递到用户配置的目标存储库里,以时序数据的形式进行存储,在目标存储库这个tab里,支持的功能有:

  • 支持查看存储目标库的地域、数据保存时间
  • 支持数据保存时间的调整

  • 点击目标库,可以进入SLS的project页面,查看原始监控数据

EBS云盘资产同步、日志采集开启之后,EBS Lens就有了EBS云盘资产和云盘的监控数据了,基于这两个数据,EBS Lens做了两个监控大盘,资源概览和性能分析页面。

多维度数据聚合与丰富的数据指标类型

  • 资源概览页面,提供一个全局的资产大盘,默认按照用户维度,提供用户账号下所有云盘的统计信息,包括:
  • 云盘总数量
  • 云盘总容量
  • 云盘实例所属地域个数
  • 云盘实例所属可用区个数
  • 启用快照云盘占比
  • 加密云盘占比
  • Top10容量的区域
  • Top10容量的可用区
  • 云盘类型容量分布
  • 付费类型容量分布

  • 除了账号的维度外,还支持对地域、付费类型、磁盘类型进行筛选,充分满足用户的各种统计需求

高精度的数据监控粒度

  • 性能分析页面提供了一个全局的云盘监控大盘,默认会统计用户账号下所有磁盘关键指标的监控,包括
  • 吞吐量
  • 总的吞吐量变化曲线
  • 读/写吞吐存储Top100的实例,以及吞吐变化曲线
  • IOPS
  • 总的IOPS变化曲线
  • 读写IOPS Top100的实例,以及IOPS变化曲线
  • 性能分析页面还支持对地域、付费类型、云盘类型、云盘id进行筛选,满足用户精细化监控的需求
  • 云盘监控粒度为10s,监控延迟为10s内,对于抖动类场景可以有效进行监控

使用场景

EBS Lens有这么便捷的管理方式以及丰富、多维度的监控指标,下面我们列举了几个常用的场景,来详细说明下EBS Lens的功能:

监控场景

下面我们模拟日常常见的磁盘IO异常的场景,展示下EBS Lens的在监控场景下的应用。

环境准备

  1. 首先我们创建一个云盘,或者使用已有的云盘,挂载到ECS实例上。挂载云盘的操作参见:help.aliyun.com/document\_d…
  2. 将账号下所有云盘通过自动化采集配置,打开监控数据采集

  1. 打开性能分析页面,确认云盘监控数据已接入进来

异常模拟

我们进入ECS实例中,使用dd来模拟一个对磁盘的异常写入的操作:

EBS Lens监控结果

在EBS Lens性能分析页面,从大盘里,我们发现有一块磁盘的吞吐量和IOPS迅速提升到TOP1。为了查看磁盘的详细指标,我们在过滤框内输入磁盘id,可以看到这个磁盘在选定时间范围内吞吐量和IOPS的变化。而该实例id,正是我们模拟的写入异常的磁盘。在线上,如果出现类似的问题,那么接下来我们就该去进行详细的问题定位,比如异常的服务日志打印、不合理的数据落盘等。通过调整时间范围,对于设置ttl范围内的数据,EBS Lens都支持在该页面进行展示,对于故障的复盘和分析也是有非常大的帮助的。

配合SLS的告警功能help.aliyun.com/document\_d…

服务压测和性能调优

除了监控场景,EBS Lens在服务压测和性能调优的场景,同样有非常大的作用。所有关于性能方面的测试,最关键的基础设施就是监控指标。EBS Lens性能分析大盘就可以提供云盘实时的性能指标,这可以有效的帮助用户快速定位云盘是否存在性能瓶颈。我们模拟一个简单的写入场景:大量级的数据要以最快的速度写到磁盘上。

环境准备

  1. 我们采用跟上面环境一样的ECS环境,在这个场景下我们指定一个固定的云盘做测试

2.在EBS Lens的页面打开该云盘的监控数据采集

场景模拟

第一版本,这里使用FIO模拟性能比较差的一个随机写的实现场景:

fio -filename=/mnt/test1 -direct=1 -iodepth 1 -thread -rw=randwrite -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=mytest

通过EBS Lens的监控,我们发现云盘的吞吐和IO比较低,远远没有达到云盘的性能上限,分别为15MB/s,900。参考块存储性能指标文档:参考help.aliyun.com/document\_d…

因此我们再对写入脚本进行优化,把随机写的实现改成了比较好的顺序写的实现:

fio -filename=/mnt/test2 -direct=1 -iodepth 1 -thread -rw=write -ioengine=psync -bs=16k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=mytest

通过EBS Lens的监控,吞吐达到了47MB/s,而IOPS达到了3000左右。

通过块存储性能指标文档我们得知,SSD云盘的性能因数据块大小而异,数据块越小,吞吐量越小,IOPS越高。因此为了提高吞吐量,我们考虑提高单次写入的数据块大小:

fio -filename=/mnt/test2 -direct=1 -iodepth 1 -thread -rw=write -ioengine=psync -bs=64k -size=2G -numjobs=10 -runtime=60 -group_reporting -name=mytest

通过EBS Lens的监控,吞吐达到了143MB/s,而IOPS下降到了2300左右。由此可以看到有了EBS Lens之后,对于磁盘IO的性能测试和调优,是多么的方便。

附录

说明

  1. EBS Lens目前处于公测期灰度开放中,如果有兴趣要试用,可以通过工单联系我们selfservice.console.aliyun.com/ticket/cate…
  2. EBS Lens公测期相关功能全部免费,公测期结束前会提前进行公示,公测结束后,费用计算可以参考help.aliyun.com/document\_d…

参考文档

  • EBS Lens帮助文档:help.aliyun.com/document\_d…
  • EBS Lens前端入口:sls.console.aliyun.com/lognext/pro…

  • SLS各个云产品日志应用文档列表:help.aliyun.com/document\_d…

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

SpringloC容器的依赖注入源码解析(9)—— popu

发表于 2021-11-26

站在设计者的角度设计populateBean: 主要实现了下面几个功能

  • 调用Bean的Setter方法实例去给Bean设置上属性值
  • 变量类型的转换,同时还要考虑处理集合类型的情况
+ 配置的时候都是以字符串的形式来配置的
  • 处理显式自动装配的逻辑(autowire = byName或byType)

请添加图片描述

用两个类来做测试,GirlFriend类中注入了BoyFriend的实例,BoyFriend中注入了自己的实例:

1
2
3
4
5
6
7
8
9
10
java复制代码package com.wjw.dao.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class BoyFriend {
@Autowired
private BoyFriend boyFriend;
}
1
2
3
4
5
6
7
8
9
10
java复制代码package com.wjw.dao.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class GirlFriend {
@Autowired
private BoyFriend boyFriend;
}

从AbstractAutowireCapableBeanFactory的doCreateBean方法中进入

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
java复制代码protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
if (bw == null) {
if (mbd.hasPropertyValues()) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
}
else {
// Skip property population phase for null instance.
return;
}
}

// Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
// state of the bean before properties are set. This can be used, for example,
// to support styles of field injection.
boolean continueWithPropertyPopulation = true;

if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}

// 如果上面设置continueWithPropertyPopulation = false,表明用户可能已经自己填充了
// bean的属性,不需要Spring帮忙填充了。此时直接返回即可
if (!continueWithPropertyPopulation) {
return;
}
// pvs是一个MutablePropertyValues实例,里面实现了PropertyValues接口,
// 提供属性的读写操作实现,同时可以通过调用构造函数实现深拷贝
// 获取BeanDefinition里面为Bean设置上的属性值
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);

PropertyDescriptor[] filteredPds = null;
if (hasInstAwareBpps) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 在这里会对@Autowired标记的属性进行依赖注入
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}
// 依赖检查,对应depend-on属性,3.0已经弃用此属性
if (needsDepCheck) {
// 过滤出所有需要进行依赖检查的属性编辑器
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
checkDependencies(beanName, mbd, filteredPds, pvs);
}

if (pvs != null) {
// 最终将属性注入到Bean的Wrapper实例里,这里的注入主要是供
// 显式配置了autowiredbyName或者ByType的属性注入,
// 针对注解来讲,由于在AutowiredAnnotationBeanPostProcessor已经完成了注入,
// 所以此处不执行
applyPropertyValues(beanName, mbd, bw, pvs);
}
}

最开始是一些判空逻辑:
请添加图片描述
接下来的逻辑就是脑图里的第一步了:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码boolean continueWithPropertyPopulation = true;

if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}

首先判断出boyfriend不是spring内部特殊的bean

1
java复制代码!mbd.isSynthetic()

再看下容器里是否已经注册了InstantiationAwareBeanPostProcessors级别的后置处理器,如果有则通过责任链模式调用这些后置处理器的postProcessAfterInstantiation方法

这里InstantiationAwareBeanPostProcessor会在属性注入之前,有最后一次机会去修改bean的属性值,此处也可以决定是否后续的填充步骤

1
java复制代码continueWithPropertyPopulation = false;
1
2
3
4
5
java复制代码// 如果上面设置continueWithPropertyPopulation = false,表明用户可能已经自己填充了
// bean的属性,不需要Spring帮忙填充了。此时直接返回即可
if (!continueWithPropertyPopulation) {
return;
}

接下来来到:

1
java复制代码PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

pvs是一个MutablePropertyValues实例,里面实现了PropertyValues接口,提供属性的读写操作实现,同时可以通过调用构造函数实现深拷贝,获取BeanDefinition里面为Bean设置上的属性值
请添加图片描述
返回给pvs的是mbd.getPropertyValues()方法的返回值,

1
2
3
4
5
6
java复制代码public MutablePropertyValues getPropertyValues() {
if (this.propertyValues == null) {
this.propertyValues = new MutablePropertyValues();
}
return this.propertyValues;
}

MutablePropertyValues类
请添加图片描述
bean的属性解析主要就是和这个类打交道的


回到populateBean方法里,第一次解析 pvs 值为空,此时就来到脑图中的第二步,按照名字 or 类型对bean进行自动装配

1
java复制代码int resolvedAutowireMode = mbd.getResolvedAutowireMode();

返回结果是0,表示没有配置上自动装配模式,意味着会跳过populateBean的自动装配逻辑。

之前介绍postProcessMergedBeanDefinition方法时,被@Value和@Autowired标记的成员变量已经被标记出来了,只差将其注入到bean实例里了,因此就没必要再走一遍解析了,所以就没必要执行if中的逻辑了。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

可以看一看上面两种注入方法的逻辑。

autowireByName:

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
java复制代码protected void autowireByName(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
// 获取要注入的非简单类型的属性名称
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
for (String propertyName : propertyNames) {
// 检测是否存在与propertyName相关的bean或BeanDefinition.
// 若存在,则调用BeanFactory.getBean方法获取bean实例
if (containsBean(propertyName)) {
// 从容器中获取相应的bean实例
Object bean = getBean(propertyName);
// 将解析出的bean存入到属性值列表pvs中
pvs.add(propertyName, bean);
// 注册依赖关系
registerDependentBean(propertyName, beanName);
if (logger.isTraceEnabled()) {
logger.trace("Added autowiring by name from bean name '" + beanName +
"' via property '" + propertyName + "' to bean named '" + propertyName + "'");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName +
"' by name: no matching bean found");
}
}
}
}

先获取非简单类型的属性名称,再根据获取出来的属性名称去依次调用容器的getBean方法获取属性名对应的bean实例,最后将获取到的bean实例连同属性的名称添加到属性列表pvs中,再将bean的依赖关系通过registerDependentBean方法给注册起来。

进入unsatisfiedNonSimpleProperties方法里:

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复制代码/**
* 获取非简单类型属性的名称,且该属性未被配置在配置文件中,如下例:将属性明确写在bean里面了,就不算自动装配
* <bean class="org.springframework.aop.framework.ProxyFactoryBean">
* <property name="target">
* <ref parent="accountService"/>
* </property>
* </bean>
*
* Spring认为的“简单类型“属性有哪些,如下:
* 1. CharSequence接口的实现类,比如String
* 2. Enum
* 3. Date
* 4. URI/URL
* 5. Number的继承类,比如Integer/Long
* 6. byte/short/int...等基本类型
* 7. Locale
* 8.以上所有类型的数组形式
*
*
* @param mbd
* @param bw
* @return
*/
protected String[] unsatisfiedNonSimpleProperties(AbstractBeanDefinition mbd, BeanWrapper bw) {
Set<String> result = new TreeSet<>();
PropertyValues pvs = mbd.getPropertyValues();
PropertyDescriptor[] pds = bw.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
if (pd.getWriteMethod() != null && !isExcludedFromDependencyCheck(pd) && !pvs.contains(pd.getName()) &&
!BeanUtils.isSimpleProperty(pd.getPropertyType())) {
result.add(pd.getName());
}
}
return StringUtils.toStringArray(result);
}

autowireByType:

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
java复制代码protected void autowireByType(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
// 获取的属性类型转换器
TypeConverter converter = getCustomTypeConverter();
if (converter == null) {
converter = bw;
}
// 用来存放解析的要注入的属性名
Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
// 获取要注入的属性名称(非简单属性(8种原始类型、字符、URL等都是简单属性))
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
for (String propertyName : propertyNames) {
try {
// 获取指定属性名称的属性Descriptor(Descriptor用来记载属性的getter setter type等情况)
PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
// Don't try autowiring by type for type Object: never makes sense,
// even if it technically is a unsatisfied, non-simple property.
// 不对Object类型的属性进行装配注入,技术上没法实现,并且没有意义
// 即如果属性类型为Object,则忽略,不做解析
if (Object.class != pd.getPropertyType()) {
// 获取属性的setter方法
MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
// Do not allow eager init for type matching in case of a prioritized post-processor.
// 对于继承了PriorityOrdered的post-processor,不允许立即初始化(热加载)
boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance());
// 创建一个要被注入的依赖描述,方便提供统一的访问
DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
// 根据容器的BeanDefinition解析依赖关系,返回所有要被注入的Bean实例
Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
if (autowiredArgument != null) {
// 将解析出的bean存入到属性值列表pvs中
pvs.add(propertyName, autowiredArgument);
}
for (String autowiredBeanName : autowiredBeanNames) {
// 注册依赖关系
registerDependentBean(autowiredBeanName, beanName);
if (logger.isTraceEnabled()) {
logger.trace("Autowiring by type from bean name '" + beanName + "' via property '" +
propertyName + "' to bean named '" + autowiredBeanName + "'");
}
}
// 清除已注入属性的记录
autowiredBeanNames.clear();
}
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
}
}
}

首先也是获得非简单类型属性名称:

1
java复制代码 String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);

然后再根据属性名获取属性描述符PropertyDescriptor:

1
java复制代码PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);

然后根据属性描述符去获取方法参数对象MethodParameter:

1
java复制代码MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);

之后再根据方法参数对象去获取依赖描述符对象DependencyDescriptor:

1
java复制代码DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);

进入PropertyDescriptor:
请添加图片描述
主要就是用来描述bean里的主要属性,可以通过它获取到该属性的getter或setter


回到autowireByType,

1
java复制代码MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);

就是获得属性setter方法实例,保存到MethodParameter实例中,该实例是封装setter的,方便后续注入,DependencyDescriptor描述要注入的依赖
请添加图片描述
InjectionPoint是用来描述注入点的,比如boyfriend中的girlfriend就是一个注入点,girlfriend的类型、setter、注解标签这些信息都可以通过InjectionPoint来获取,DependencyDescriptor除了上述的功能外,还保存了一些额外的信息,比如:依赖是否必要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public boolean isRequired() {
if (!this.required) {
return false;
}

if (this.field != null) {
return !(this.field.getType() == Optional.class || hasNullableAnnotation() ||
(KotlinDetector.isKotlinReflectPresent() &&
KotlinDetector.isKotlinType(this.field.getDeclaringClass()) &&
KotlinDelegate.isNullable(this.field)));
}
else {
return !obtainMethodParameter().isOptional();
}
}

是否是延迟加载的:

1
2
3
java复制代码public boolean isEager() {
return this.eager;
}

相关的一些嵌套级别:

1
2
3
4
5
6
7
java复制代码public void increaseNestingLevel() {
this.nestingLevel ++;
this.resolvableType = null;
if (this.methodParameter != null) {
this.methodParameter = this.methodParameter.nested();
}
}

回到autowireByType,获取到DependencyDescriptor之后就可以进行属性对应的值的解析了

1
2
java复制代码// 根据容器的BeanDefinition解析依赖关系,返回所有要被注入的Bean实例
Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);

autowiredArgument是属性的值,并将其与属性名一起添加到属性列表中

1
java复制代码pvs.add(propertyName, autowiredArgument);

之后再注册一下bean之间的依赖关系

1
java复制代码registerDependentBean(autowiredBeanName, beanName);

毕竟不是所有的依赖关系都是在配置里显式标注的。注册完成后就完成了该方法

本文转载自: 掘金

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

死磕synchronized五:系统剖析轻量级锁 关于轻量级

发表于 2021-11-26

哈喽,大家好,我是江湖人送外号[道格牙]的子牙老师。

近期准备写一个专栏:从Hotspot源码角度剖析synchronized。前前后后大概有10篇,会全网发,写完后整理成电子书放公众号供大家下载。对本专栏感兴趣的、希望彻彻底底学明白synchronized的小伙伴可以关注一波。电子书整理好了会通过公众号群发告知大家。我的公众号:硬核子牙。

本篇文章给大家分享关于轻量级锁的所有。在synchronized的所有锁类型中,轻量级锁是最简单的,但是这个简单是相对的,如果不具备一些底层的思维及学习底层的办法,它的源码也不是那么容易理解。

关于轻量级锁

首先说下什么情况会用到轻量级锁

  1. 延迟偏向期间,偏向锁还不可用,synchronized首先拿到的就是轻量级锁。即由无锁升级成轻量级锁
  2. 抢占偏向锁失败的线程会触发锁膨胀至轻量级锁。这里还需要考虑偏向锁重入的情况
  3. 膨胀成轻量级锁以后,如果后续的线程是在持有锁的线程执行结束后来抢锁,拿到的依然是轻量级锁。因为释放轻量级锁会恢复成无锁
  4. 膨胀成轻量级锁以后,如果是来抢锁,就会触发膨胀成重量级锁

再说下轻量级锁在对象头中的存在形式,如图。

针对大家的这么几个疑惑说明一下:1、64位操作系统,内存地址是8字节,64位,为什么能用62位保存?因为内存地址虽然可以用64位,实际上只用了48位,还有16位是保留位,所以完全可以;2、为什么可以用尾两位的00表示轻量级锁?言外之意就是说内存地址的尾两位一定是00吗?是的。64位操作系统,线程栈是按8字节为一个单位来用的,所以尾两位一定是00。

很多小伙伴还有一个疑惑,轻量级锁对象到底存放在虚拟机栈的什么位置?从这张图也可以看出来,是存放在栈帧的顶部。如果该方法被synchronized修饰,或者方法中有synchronized代码段,就会在栈帧的顶部创建一块叫monitor block区域,专门用来存放lock record。

接下来看下Hotspot源码是如何实现轻量级锁的、如何处理轻量级锁的重入、如何处理偏向锁膨胀成轻量级锁的兼容。

轻量级锁前的堆栈图

如果是synchronized修饰的方法,会先执行lock_method方法。这个方法做了三件事情:

  1. 根据是否是静态方法计算出锁对象,将锁对象的内存地址保持到rax寄存器中
  2. 在当前的栈帧中创建一个lock record,将锁对象的内存地址写入
  3. 调用lock_object完成加锁

lock_method执行之后的堆栈图

lock_object

这个方法是模板解释器处轻量级锁的抢锁与重入处理逻辑。我们Java代码中的synchronized的轻量级锁都是在这里处理的。

这个方法全是汇编,我就不贴源码了,以伪代码的方式直接讲它的执行逻辑

image.png

这个方法执行后,如果不发生重入,堆栈图还是上面那个,不会变,只是将lock record的内存地址通过CAS写入到对象头中。

如果是重入,CAS是失败的,对象头不会发生变化,堆栈图会发生变化:

  1. 如果是synchronized修饰方法这种情况,一个栈帧一个lock record,区别是第一个BasicLock.display_header不为0,其他都为0。为什么要这样呢?为了释放锁的时候识别哪个是最后一层
  2. 如果是synchronized代码块,一个栈帧中就有多个lock record,最后一层的BasicLock.display_header不为0,其他都为0。如图

轻量级锁解锁流程

轻量级锁的解锁逻辑比较简单:

  1. 判断是不是重入,如果是重入,直接return;
  2. 如果不是重入,意味着在解最后一层轻量级锁,需要将对象头恢复成无锁,这样后面的线程才能拿到轻量级锁。这个无锁头从哪来的?回忆一下上锁的时候,把rax寄存器中的无锁头写入了BasicLock.display_header,就是从这拿的;
  3. 其他情况就按重量级锁解锁处理。

image.png

偏向锁膨胀与重入

如果是偏向锁膨胀成轻量级锁,或者是重入的偏向锁膨胀成轻量级锁,Hotspot是如何处理的呢?直接看源码。注释写的很详细,就不重复啰嗦了。

image.png

至此就把轻量级锁相关的知识点讲完了。

系列文章

1、JVM如何执行synchronized修饰的方法

2、死磕synchronized二:系统剖析延迟偏向篇一

3、死磕synchronized三:系统剖析延迟偏向篇二

4、死磕synchronized四:系统剖析偏向锁篇一

推荐阅读

1、今天聊点不一样的,百万年薪需要具备的能力

2、你是不是想问,那些技术大牛是如何练成的?我来告诉你

3、深入剖析Lambda表达式的底层实现原理

结语

结语

其实技术这个行业真的不难,如果有人带,打底子1-2年,沉淀2-3年,足矣。我自己一步步探索,走得还算顺利,大概花了七年时间。

给大家看看我写好的

image.png

image.png

image.png

本文转载自: 掘金

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

Kafka生产者消息发送流程 消息发送示例 发送流程分析 结

发表于 2021-11-26

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

消息发送示例

使用kafka生产者发送一条消息的时候,示例代码,可能如下:

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
java复制代码    public static void main(String[] args) throws InterruptedException {
String server = "localhost:9092";
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, server);
props.put(ProducerConfig.CLIENT_ID_CONFIG, "DemoProducer");

props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 安全认证配置,如果需要会这样配置,未启用ACL,忽略这几项配置
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT");
props.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256");
props.put(SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.scram.ScramLoginModule required username="admin" password="admin";");

KafkaProducer<Integer, String> producer = new KafkaProducer<>(props);

String topic = "test_topic";
String message = "hello, kafka";
producer.send(new ProducerRecord<>(topic, message), (metadata, exception) -> {
log.info("metadata: {}", metadata);
if (exception != null) {
log.error("send exception: ", exception);
}
});

Thread.currentThread().join();
}

示例代码,仅供参考,下面分析一下消息是如何发送出去的。

发送流程分析

鉴于代码量太大,所以不会贴太多源码分析,尽量使用流程图和文字表达清楚。

查看源码

基本流程

示例代码中,消息发送整体分两个步骤:

  1. 构造生产者实例
  2. 消息发送

下面对这两个过程分析

创建生产者

创建流程

示例中,创建的关键代码就这一行 :

1
java复制代码KafkaProducer<Integer, String> producer = new KafkaProducer<>(props);

主要流程如流程图

  1. 调用KafkaProducer接口构造生产者实例
  2. 将配置的Properties转换为ProducerConfig,kafka生产者的配置属性有很多,如果我们没有配置的属性就会使用默认配置
  3. 初始化生产者实例的各个属性,如上面注释中图片里显示的这些属性
  4. 启动消息发送线程,完成

这里面有两个属性需要本文重点关注,与消息发送有直接关系(比如监控统计类属性,就不算直接关系,因为即使没有,消息也可以发送,就这个意思):

这两个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码            // 缓存待发送的消息
this.accumulator = new RecordAccumulator(logContext,
config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
this.compressionType,
lingerMs(config),
retryBackoffMs,
deliveryTimeoutMs,
metrics,
PRODUCER_METRIC_GROUP_NAME,
time,
apiVersions,
transactionManager,
new BufferPool(this.totalMemorySize, config.getInt(ProducerConfig.BATCH_SIZE_CONFIG), metrics, time, PRODUCER_METRIC_GROUP_NAME));
// 进行实际的消息发送运作
this.sender = newSender(logContext, kafkaClient, this.metadata);

消息发送

生产者-消费者模式发送消息

上面提到了producer有两个属性:accumulator和sender。

kafka producer发送消息并不是同步发送的,而类似于生产者-消费者模式的异步消息发送:

当produer调用send方法,发送消息的时候,只是先把消息缓存到一个队列,由该模式的消费者(另一个线程)来消费消息并执行真正的发送逻辑。这样主要是为了发送的时候尽量是批次的消息发送,而非单条单条消息的发送用来提升发送性能。

prdoucer实例就相当于该模式的生产者,accumulator是其中的缓存队列,sender便是消费者。

sender是在一个异步线程(ioThread)中执行主要逻辑,不停的从accumulator中获取准备发送的消息批次并通过网络发送到目标broker上,基本流程如下:

所以,到这里,也是主要分两步:

  1. producer调用send方法把消息放入accumulator
  2. sender从accumulator拿到消息发送到kafka broker

消息放入accumulator

我们发送消息可能这样写:

1
2
3
4
5
6
java复制代码        producer.send(new ProducerRecord<>(topic, message), (metadata, exception) -> {
log.info("metadata: {}", metadata);
if (exception != null) {
log.error("send exception: ", exception);
}
});

看一下调用send方法后的执行流程:

消息实际是批量发送,从流程图里可以看到一个批次的消息满足条件才会唤醒sender准备发送,接下来看一下sender实际进行消息发送的流程。

消息批量异步发送

上文说明sender是在调用 new KafkaProducer()构造producer实例的时候,初始化的,代码如下:

1
2
3
4
java复制代码            this.sender = newSender(logContext, kafkaClient, this.metadata);
String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();

是在一个异步线程(ioThread)内执行相关逻辑,是一个死循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码    @Override
public void run() {
log.debug("Starting Kafka producer I/O thread.");

// main loop, runs until close is called
while (running) {
try {
// 调用Sender.runOnce()方法
runOnce();
} catch (Exception e) {
log.error("Uncaught error in kafka producer I/O thread: ", e);
}
}

log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records.");

// 下面执行关闭逻辑,代码忽略
}

在Sender.runOnce方法里不停的调用发送生产消息的请求:

1
2
3
4
java复制代码
long currentTimeMs = time.milliseconds();
long pollTimeout = sendProducerData(currentTimeMs);
client.poll(pollTimeout, currentTimeMs);

看这两行代码,是否会奇怪调用sendProducerData方法后,又调用下面这个client.poll方法?

我简单解释下:

  1. kafka的网络通信框架是自己基于java nio封装的一套实现,不是像rocketmq等用的netty那一套
  2. 调用sendProducerData方法只是把要发送的请求准备好,还未进行实际的网络传输
  3. 调用client.poll方法会进行实际io操作,将所有channel待发送数据通过socket发送出去

基本流程大概是这样:

结语

消息的发送过程基本就是这样,主要采用的就是生产者-消费者模式,异步批量发送的形式。

其中处理过程中还是有特别多的细节,不再全部展开来说了。

示例代码中,配置了ACL认证,kafka的认证机制是在连接连接建立的时候做的。如果是默认的话,就是没有认证且明文传输,关键的握手那一块是空实现,这个会放在其实篇单独说明。它不像rocketmq那样,rocketmq是请求的时候每次都会带上相关秘钥进行权限认证。

本文转载自: 掘金

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

网络协议-路由

发表于 2021-11-26
  • 基础概念

路由的作用就是在不同网段之间转发数据的

但是默认情况下路由器只知道和他直连的网段非直连的网段需要通过静态路由或者是动态路由告诉他
如下图所示:

首先要注意一个点两个路由器直连下图的两个接口都需要有IP地址并且要在同一个网段
image.png

下图中可以看到计算机2和你算计3、4可以直接传输信息的应为他们虽然不是在同一个网段,但是他们都是和路由器4直连的设备,所以可以直接传输信息如下图所示
iShot2021-11-25 08.54.41.gif

但是如果计算机2想和计算机0通信那么一定是失败的应为计算机0不是路由器4直连的设备所以通信会失败如下图
屏幕录制2021-11-25 上午9.04.05 (1).gif
如上图可以发现在路由器哪里就直接报错了

  • 动态路由

动态路由比较简单,路由器通过选择路由协议(比如RIP、OSPF)自动获取路由信息

直白点说就是通过协议去问和自己路由直连的路由他的直连网段,一直找到对应的路由器然后发送信息

  • 静态路由

需要手动添加路由信息,也就是说配置路由表,需要告诉路由你的目标网段的下一跳是哪里如下图所示(配置路由器4的路由表)
image.png
当然如果想要通信成功只配置路由器4的静态路由还不够,应为这个时候消息只能发过去不能回来应为路由器5不知道回来的路怎么走所以还要配置路由器5的静态路由如下图所示:
image.png
此时计算机2就能和计算机0与计算机1通信了如下图:
屏幕录制2021-11-26 上午9.31.26 (1).gif
但是计算机3和计算机4一样是无法和计算机0和计算机1通信的应为路由器5没有配置计算机3和计算机4的网段,这里可以再路由5中再添加一条计算机3和计算机4的网段如下图:
image.png
此时就可以通信了

这里路由器还有几种方法

+ 子网掩码往前移动例如计算机2、3、4是两个网段分别是192.168.2.0和192.168.3.0子网掩码都是255.255.255.0,此时路由就需要添加两个网段信息,我们也可以将子网掩码配置成255.255.0.0,也就是满足192.168.0.0网段的都能走下一跳,这样就不需要配置两条信息而是配置一条就行如下图:
![image.png](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/b49cb8574d2bdf51437017116633fe6e3a1874c6f06af5ae647424348035ca2a)
此时计算机2、3、4都可以和计算机0、1通信了如下图所示:
![屏幕录制2021-11-26 上午9.45.22 (1).gif](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/d943feb4bb6d4958588d3c7871c7768acdd82423f8cab0f1820fa75b0ba06ccb)
+ 也可以配置网段0.0.0.0子网掩码0.0.0.0,这样配置的意思就是只要不是与路由器直连的网段都会走配置的下一跳
+ 也可以精确到对应的ip,例如配置计算机2的 ip就是192.168.2.2,子网掩码是255.255.255.255

本文转载自: 掘金

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

【每日算法】力扣121 买卖股票的最佳时机

发表于 2021-11-26

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战。

描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

1 <= prices.length <= 105

0 <= prices[i] <= 104

通过次数605,132 提交次数1,055,538

做题

这道题目需要我们做两件事情,一件事寻找一个最低的买入价格,另外就是找到最高的收益。

注意我们的买和卖都只有一次机会。

寻找合适的买入价格,不断地拿当前的数去比较,如果当前的数比较小就替换;

如果当前的数比较大,卖出价格就是当前的价格(因为买入价格是前面的数,所以可以卖出),相减看看有多少收益,是负数就不用考虑了,然后再拿这个收益和最大收益比较。

这一趟循环下来就可以得到最大的收益了。

敲码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码public int maxProfit(int[] prices) {
int maxProfit=0;
//把第一个价格当作最低售价,这样就可以直接开始找最高的收益了
int minPrice =prices[0];
for (int i = 1; i < prices.length; i++) {
int price = prices[i];
if (price<minPrice){
//当前的数比较小
minPrice=price;
continue;
}
int profit = price - minPrice;
//寻找最大的收益
maxProfit=profit>maxProfit?profit:maxProfit;
}
return maxProfit;
}

image.png

一次做对,还做的这么好!我真棒!

今天就到这里了。

这里是程序员徐小白,【每日算法】是我新开的一个专栏,在这里主要记录我学习算法的日常,也希望我能够坚持每日学习算法,不知道这样的文章风格您是否喜欢,不要吝啬您免费的赞,您的点赞、收藏以及评论都是我下班后坚持更文的动力。

本文转载自: 掘金

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

Thread中断机制interrupt

发表于 2021-11-26

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

一、中断线程

线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,标记线程处于中断状态,但不会终止线程,线程还会继续执行。中断的结果线程是死亡,还是等待新的任务或是继续运行至下一步,取决于这个程序本身。线程会不时地检测这个中断标示位,以判断是否应该被中断(中断标示值是否为true)。

​

二、判断线程是否被中断

判断某个线程是否已被发送过中断请求,使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标示设置为false),不要使用thread.interrupted()(检查当前线程是否已经中断,线程的中断状态由该方法清除,即将中断标示设置为false,如果连续两次调用该方法,则第二次调用将返回false,在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)

​

三、如何中断线程

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait以及可中断的通道上的IO操作方法后可进入阻塞状态),则线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

​

中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断,某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是普遍的情况是,一个线程将把中断看作一个终止请求。

​

四、中断应用

1)使用中断信号量中断非阻塞状态的线程

使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。

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
arduino复制代码public class ThreadExample {

    public static void main(String[] args) throws  Exception{

        InterruptThreadBySemaphore interruptThreadBySemaphore = new InterruptThreadBySemaphore();
        interruptThreadBySemaphore.start();
        Thread.sleep(3000);
        System.out.println("asking thread to stop");
        //设置中断信号量
        interruptThreadBySemaphore.stop = true;
        Thread.sleep(3000);
        System.out.println("stopping application...");
    }
     static class InterruptThreadBySemaphore extends Thread {

         /**
          * 线程中断信号量
          */
         volatile boolean stop = false;

         @Override
         public void run() {
             super.run();
             //每隔一秒检测一下中断信号量
             while (!stop) {
                 System.out.println("thread is running....");
                 long time = System.currentTimeMillis();
                 /**
                  * 使用while循环模拟sleep方法,使用sleep,否则在阻塞时会抛
                  * InterruptedException异常而退出循环
                  */
                 while ((System.currentTimeMillis() - time) < 1000) {

                 }
             }
             System.out.println("thread exiting...");
         }
     }
}

2)使用thread.interrupt()中断非阻塞状态线程

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
csharp复制代码public class ThreadExample {

    public static void main(String[] args) throws  Exception{

        InterruptThreadByInterrupt interruptThreadByInterrupt = new InterruptThreadByInterrupt();
        interruptThreadByInterrupt.start();
        Thread.sleep(3000);
        System.out.println("asking thread to stop");
        //发起中断请求
        interruptThreadByInterrupt.interrupt();
        Thread.sleep(3000);
        System.out.println("stopping application...");
    }

    static class InterruptThreadByInterrupt extends Thread {

        @Override
        public void run() {
            super.run();
            //每隔一秒检测一下中断信号量
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("thread is running....");
                long time = System.currentTimeMillis();
                /**
                 * 使用while循环模拟sleep方法,使用sleep,否则在阻塞时会抛
                 * InterruptedException异常而退出循环
                 */
                while ((System.currentTimeMillis() - time) < 1000) {

                }
            }
            System.out.println("thread exiting...");
        }
    }
}

3)使用thread.interrup()中断阻塞状态线程

Thread.interrupt()方法不会中断一个正在运行的线程。是设置线程的中断标示位,在线程受阻塞的地方(如sleep,wait,join等)抛出一个异常InterruptedException,并且中断状态也将被清除,这样线程就得以退出阻塞的状态。

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
csharp复制代码public class ThreadExample {

    public static void main(String[] args) throws  Exception{

        InterruptBlockingThread interruptBlockingThread = new InterruptBlockingThread();
        interruptBlockingThread.start();
        Thread.sleep(3000);
        System.out.println("asking thread to stop");
        interruptBlockingThread.interrupt();
        Thread.sleep(3000);
        System.out.println("stopping application...");
    }


    static class InterruptBlockingThread extends Thread {

        @Override
        public void run() {
            super.run();
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("thread is running....");
                try {
                    /**
                     * 如果线程阻塞,将不会去检查中断信号量stop,所以thread.interrupt()
                     * 会使用阻塞线程从阻塞的地方抛出异常,让阻塞线程从阻塞状态逃离出来,
                     * 并进行异常块进行,相应的处理
                     */
                    Thread.sleep(1000);//线程阻塞,如果线程收到中断操作信号将抛出异常
                }catch (InterruptedException e){
                    System.out.println("thread interrupted....");
                    /**
                     * 如果线程在调用Object.wait()方法,或者该类的join(),sleep()方法
                     * 过程中受阻,则其中断状态将被清除
                     */
                    System.out.println(this.isInterrupted());//false
                    /**
                     * 中不中断由自己决定,如果需要中断线程,则重新设置中断位,
                     * 如果不需要,则不用调用
                     */
                    Thread.currentThread().interrupt();
                }

            }
            System.out.println("thread exiting...");
        }
    }
}

运行结果:\

image.png

image.png

五、总结

Java的中断是一种协作机制。也就是说调用线程对象的interrupt()方法并不一定就中断了正在运行的线程,它只是要求线程在合适的时机中断自己。每个线程都有一个boolean的中断状态,interrupt()方法仅仅只是将该状态置为true,比如对正常运行的线程调用interrupt()并不能终止它,只是改变了interrupt标示符。

一般来说,如果一个方法声明抛出InterruptedException,表示该方法是可中断的,比如wait,sleep,join,也就是说可中断方法会对interrupt调用做出响应(例如sleep响应interrupt的操作包括清除中断状态,抛出InterruptedException),异常都是由可中断方法自己抛出来的,并不是直接由interrupt()方法直接引起的。

Object.wait(),Thread.sleep()方法,会不断的轮询监听interrupted标志位,发现其设置为true后,会停止阻塞并抛出InterruptedException异常。

本文转载自: 掘金

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

LeetCode-129-求根节点到叶节点数字之和

发表于 2021-11-26

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」。

求根节点到叶节点数字之和

题目描述:给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

  • 例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
    计算从根节点到叶节点生成的 所有数字之和 。

叶节点 是指没有子节点的节点。

示例说明请见LeetCode官网。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/su…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一:递归法

使用递归法解决此问题,递归过程如下:

  • 首先,如果当前节点为null,说明是空树,直接返回;
  • 如果当前节点不是nll,将当前节点的值添加到 path 中;
  • 然后判断当前节点没有左右子节点,说明是叶子节点,将当前的路径值加到result中,然后返回;
  • 如果当前节点的左节点不为空时,递归处理左节点;
  • 如果当前节点的右节点不为空时,递归处理右节点。

最后,返回result即为结果值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码import com.kaesar.leetcode.TreeNode;

public class LeetCode_129 {
// 最终的累加值
private static int result = 0;

public static int sumNumbers(TreeNode root) {
sumNumbers(root, "");
return result;
}

/**
* 递归法
*
* @param root
* @param path
*/
private static void sumNumbers(TreeNode root, String path) {
// 如果当前节点为null,说明是空树,直接返回
if (root == null) {
return;
}
// 将当前节点的值添加到 path 中
path += root.val;
// 如果当前节点没有左右子节点,说明是叶子节点,将当前的路径值加到result中,然后返回
if (root.left == null && root.right == null) {
result += Integer.valueOf(path);
return;
}
if (root.left != null) {
// 当前节点的左节点不为空时,递归处理左节点
sumNumbers(root.left, path);
}
if (root.right != null) {
// 当前节点的右节点不为空时,递归处理右节点
sumNumbers(root.right, path);
}
}

public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(9);
root.right = new TreeNode(0);
root.left.left = new TreeNode(5);
root.left.right = new TreeNode(1);

// 测试用例,期望输出: 1026
System.out.println(sumNumbers(root));
}
}

【每日寄语】 人生就像一场赌局,不可能把把都赢,只要筹码在自己手上,就永远都会有希望。

本文转载自: 掘金

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

ElasticSearch——京东商城搜索项目实战

发表于 2021-11-26

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

技术栈:

SpringBoot 2.5.6

ElasticSearch 7.8.0

Vue

1、项目搭建

1、新建一个Spring Boot 项目

2、导入依赖

​ 2.1、 修改Springboot中的ElasticSearch版本和本地的版本一致

1
2
3
4
java复制代码<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.8.0</elasticsearch.version>
</properties>

​ 2.2、导入依赖

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
xml复制代码<dependencies>
<!-- jsoup解析网页-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.2</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<!-- ElasticSearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- devtools热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- lombok 需要安装插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

3、编写配置文件

1
2
3
4
properties复制代码# 更改端口,防止冲突
server.port=9090
# 关闭thymeleaf缓存
spring.thymeleaf.cache=false

4、导入静态资源

链接:pan.baidu.com/s/10EF40UUK…
提取码:ot8j

在这里插入图片描述

5、测试访问静态页面

1
2
3
4
java复制代码@GetMapping({"/","/index"})
public String test(){
return "index";
}

访问请求:http://localhost:9090/

在这里插入图片描述

项目搭建完成!

2、爬取数据

1、通过请求 search.jd.com/Search?keyw… 查询到页面

在这里插入图片描述

检查网页:可以看到元素列表id为 J_goodsList,

在这里插入图片描述

每个li标签里面存放了每个商品的具体数据:

在这里插入图片描述

2、解析页面,获取数据

编写解析页面工具类,获取商品信息:简易版

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复制代码package com.cheng.utils;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

/**
* @Author wpcheng
* @Create 2021-11-10-9:46
*/
public class HTMLParseUtil {
public static void main(String[] args) throws IOException {
//1.获取请求
String url = "https://search.jd.com/Search?keyword=java";
//2、解析网页 这里Jsoup返回的就是浏览器的Document对象,在这里可以用js里的方法
Document document = Jsoup.parse(new URL(url), 30000);
//获取"J_goodsList"列表
Element element = document.getElementById("J_goodsList");
//获取"J_goodsList"列表中的li标签集合
Elements elements = element.getElementsByTag("li");
//将li标签集合中的每一个li标签遍历出来,一个el里有一个商品的信息
for (Element el : elements) {

//一般图片特别多的网站,所有的图片都是通过延迟加载的,图片地址放在"data-lazy-img"里面
String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img");//获取商品图片的地址
String price = el.getElementsByClass("p-price").eq(0).text();//获取商品的价格
String title = el.getElementsByClass("p-name").eq(0).text();//获取商品的标题

System.out.println("=====================================");
System.out.println(img);
System.out.println(price);
System.out.println(title);
}
}
}

运行:数据获取成功。

在这里插入图片描述

编写商品信息的实体类:

1
2
3
4
5
6
7
8
9
10
java复制代码@Data
@NoArgsConstructor
@AllArgsConstructor
public class Content implements Serializable {
private static final long serialVersionUID = -8049497962627482693L;
private String name;
private String img;
private String price;

}

将解析页面工具类 HTMLParseUtil 封装成方法:完整版

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
java复制代码package com.cheng.utils;

import com.cheng.pojo.Content;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class HTMLParseUtil {
public static void main(String[] args) throws Exception {
//测试
new HTMLParseUtil().parseJD("刘同").forEach(System.out::println);

}

public List<Content> parseJD(String keyword) throws Exception {

String url = "https://search.jd.com/Search?keyword="+keyword;
//2、解析网页 这里Jsoup返回的就是浏览器的Document对象,在这里可以用js里的方法
Document document = Jsoup.parse(new URL(url), 30000);
//获取"J_goodsList"列表
Element element = document.getElementById("J_goodsList");
//获取"J_goodsList"列表中的li标签集合
Elements elements = element.getElementsByTag("li");
//将li标签集合中的每一个li标签遍历出来,一个el里有一个商品的信息
// list存储所有li下的内容
List<Content> contents = new ArrayList<>();
for (Element el : elements) {
//一般图片特别多的网站,所有的图片都是通过延迟加载的,图片地址放在"data-lazy-img"里面
String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img");//获取商品图片的地址
String price = el.getElementsByClass("p-price").eq(0).text();//获取商品的价格
String title = el.getElementsByClass("p-name").eq(0).text();//获取商品的标题

Content content = new Content(img, price, title);
contents.add(content);
}

return contents;
}
}

运行测试工具类:

在这里插入图片描述

爬取数据测试成功!

3、编写业务

1、编写配置文件

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
public class ESHighClientConfig {

@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));
return client;
}

}

2、编写service层

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
java复制代码package com.cheng.service;

import com.alibaba.fastjson.JSON;
import com.cheng.pojo.Content;
import com.cheng.utils.HTMLParseUtil;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;


@Service
public class ContentService {

@Autowired
RestHighLevelClient restHighLevelClient;

//爬取数据放入索引
public boolean parseContent(String keyword) throws Exception {
//用自定义解析工具类解析网页,获取数据
List<Content> contents = HTMLParseUtil.parseJD(keyword);

//将解析得到的数据批量加入ES中
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("2m");
for (int i = 0; i < contents.size(); i++) {
bulkRequest.add(
new IndexRequest("jd_goods")
.source(JSON.toJSONString(contents.get(i)), XContentType.JSON));

}
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

return !bulkResponse.hasFailures();//返回true执行成功
}


//搜索文档信息
public List<Map<String,Object>> search(String keyword,int pageIndex,int pageSize) throws IOException {

if (pageIndex < 0){
pageIndex = 0;
}

//针对索引构建查询请求
SearchRequest request = new SearchRequest("jd_goods");

//构建搜索条件
SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource();
//精确查询关键词
TermQueryBuilder termQuery = QueryBuilders.termQuery("name", keyword);
//把精确查询放入搜索条件
sourceBuilder.query(termQuery).timeout(new TimeValue(60, TimeUnit.SECONDS));

//分页
sourceBuilder.from(pageIndex);
sourceBuilder.size(pageSize);

//把搜索条件放入请求
request.source(sourceBuilder);

//执行查询请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

List<Map<String,Object>> list = new ArrayList<>();

for (SearchHit documentFields : response.getHits().getHits()) {
//把每个商品信息当做map取出
Map<String, Object> sourceAsMap = documentFields.getSourceAsMap();
//放入list集合
list.add(sourceAsMap);
}
return list;
}

}

3、编写controller层

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复制代码package com.cheng.controller;

import com.cheng.service.ContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@RestController
public class TextController {

@Autowired
private ContentService contentService;


@GetMapping({"/","/index"})
public String test(){
return "index";
}


@GetMapping("/parse/{keyword}")
public Boolean parse(@PathVariable("keyword") String keyword) throws Exception {
return contentService.parseContent(keyword);
}


@GetMapping("/search/{keyword}/{pageIndex}/{pageSize}")
public List<Map<String, Object>> parse(@PathVariable("keyword") String keyword,
@PathVariable("pageIndex") Integer pageIndex,
@PathVariable("pageSize") Integer pageSize) throws IOException {
return contentService.search(keyword,pageIndex,pageSize);
}

}

4、进行测试

访问请求:http://localhost:9090/parse/java 添加文档:文档数据已添加进ES中

在这里插入图片描述

访问请求:http://localhost:9090/search/java/1/20 查询带有关键字“java”的商品信息,并进行分页:查询成功!

在这里插入图片描述

4、前后端交互

1、导入vue和axios依赖

在这里插入图片描述

2、引入js到html文件中

1
2
html复制代码<script th:src="@{/js/vue.min.js}"></script>
<script th:src="@{/js/axios.min.js}"></script>

3、渲染后的 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
html复制代码<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>狂神说Java-ES仿京东实战</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
<script th:src="@{/js/jquery.min.js}"></script>
</head>
<body class="pg">
<div class="page">
<div id="app" class=" mallist tmall- page-not-market ">
<!-- 头部搜索 -->
<div id="header" class=" header-list-app">
<div class="headerLayout">
<div class="headerCon ">
<!-- Logo-->
<h1 id="mallLogo">
<img th:src="@{/images/jdlogo.png}" alt="">
</h1>
<div class="header-extra">
<!--搜索-->
<div id="mallSearch" class="mall-search">
<form name="searchTop" class="mallSearch-form clearfix">
<fieldset>
<legend>天猫搜索</legend>
<div class="mallSearch-input clearfix">
<div class="s-combobox" id="s-combobox-685">
<div class="s-combobox-input-wrap">
<input v-model="keyword" type="text" autocomplete="off" id="mq"
class="s-combobox-input" aria-haspopup="true">
</div>
</div>
<button type="submit" @click.prevent="searchKey" id="searchbtn">搜索</button>
</div>
</fieldset>
</form>
<ul class="relKeyTop">
<li><a>狂神说Java</a></li>
<li><a>狂神说前端</a></li>
<li><a>狂神说Linux</a></li>
<li><a>狂神说大数据</a></li>
<li><a>狂神聊理财</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 商品详情页面 -->
<div id="content">
<div class="main">
<!-- 品牌分类 -->
<form class="navAttrsForm">
<div class="attrs j_NavAttrs" style="display:block">
<div class="brandAttr j_nav_brand">
<div class="j_Brand attr">
<div class="attrKey">
品牌
</div>
<div class="attrValues">
<ul class="av-collapse row-2">
<li><a href="#"> 狂神说 </a></li>
<li><a href="#"> Java </a></li>
</ul>
</div>
</div>
</div>
</div>
</form>
<!-- 排序规则 -->
<div class="filter clearfix">
<a class="fSort fSort-cur">综合<i class="f-ico-arrow-d"></i></a>
<a class="fSort">人气<i class="f-ico-arrow-d"></i></a>
<a class="fSort">新品<i class="f-ico-arrow-d"></i></a>
<a class="fSort">销量<i class="f-ico-arrow-d"></i></a>
<a class="fSort">价格<i class="f-ico-triangle-mt"></i><i class="f-ico-triangle-mb"></i></a>
</div>
<!-- 商品详情 -->
<div class="view grid-nosku" >
<div class="product" v-for="result in results">
<div class="product-iWrap">
<!--商品封面-->
<div class="productImg-wrap">
<a class="productImg">
<img :src="result.img">
</a>
</div>
<!--价格-->
<p class="productPrice">
<em v-text="result.price"></em>
</p>
<!--标题-->
<p class="productTitle">
<a v-html="result.name"></a>
</p>
<!-- 店铺名 -->
<div class="productShop">
<span>店铺: 狂神说Java </span>
</div>
<!-- 成交信息 -->
<p class="productStatus">
<span>月成交<em>999笔</em></span>
<span>评价 <a>3</a></span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script th:src="@{/js/axios.min.js}"></script>
<script>
new Vue({
el:"#app",
data:{
keyword: '', // 搜索的关键字
results:[] // 后端返回的结果
},
methods:{
searchKey(){
var keyword = this.keyword;
console.log(keyword);
axios.get('h_search/'+keyword+'/0/20').then(response=>{
console.log(response.data);
this.results=response.data;
})
}
}
});
</script>
</body>
</html>

5、关键字高亮

在ContentService里面加上关键字高亮的方法:

原理:用新的高亮字段值覆盖旧的字段值

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复制代码public List<Map<String, Object>> highlightSearch(String keyword, Integer pageIndex, Integer pageSize) throws IOException {
//针对构建查询请求
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 精确查询
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", keyword);
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//添加查询
searchSourceBuilder.query(termQueryBuilder);
// 分页
searchSourceBuilder.from(pageIndex);
searchSourceBuilder.size(pageSize);
// 关键字高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("name");
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
searchSourceBuilder.highlighter(highlightBuilder);
//添加查询条件到查询请求
searchRequest.source(searchSourceBuilder);
// 执行查询请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果
SearchHits hits = searchResponse.getHits();
List<Map<String, Object>> results = new ArrayList<>();
for (SearchHit documentFields : hits.getHits()) {
// 使用新的高亮字段值覆盖旧的字段值
Map<String, Object> sourceAsMap = documentFields.getSourceAsMap();
// 高亮字段
Map<String, HighlightField> highlightFields = documentFields.getHighlightFields();
HighlightField name = highlightFields.get("name");
// 开始替换
if (name != null){
Text[] fragments = name.fragments();
//用StringBuilder效率更高
StringBuilder new_name = new StringBuilder();
for (Text text : fragments) {
new_name.append(text);
}
sourceAsMap.put("name",new_name.toString());
}
results.add(sourceAsMap);
}
return results;
}

在index.html中配置高亮效果:

1
2
3
4
html复制代码<!--标题-->
<p class="productTitle">
<a v-html="result.name"></a>
</p>

6、最终效果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

本文转载自: 掘金

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

ElasticSearch——Spring Boot 集成

发表于 2021-11-26

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

1、SpringBoot 集成 ES

1、新建一个Spring Boot 项目,添加 ElasticSearch 模块:

在这里插入图片描述

2、修改ES版本

可以看到,SpringBoot帮我们导入的ES版本是7.12.1:

在这里插入图片描述

而我们本地使用的版本是7.8.0,所以为了防止连接ES失败,要把版本号修改过成和本地一致。

1
2
xml复制代码<!-- 在pom.xml文件的properties属性下面添加: -->
<elasticsearch.version>7.8.0</elasticsearch.version>

一定要确保修改成功。

3、编写配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.cheng;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.RestClients;

@Configuration
public class ElasticSearchClientConfig {


//配置RestHighLevelClient依赖到spring容器中待用
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
//绑定本机,端口,协议,如果是ES集群,就配置多个
new HttpHost("127.0.0.1",9200,"http")));
return client;
}
}

到这里,ElasticSearch和SpringBoot的集成就完成了,下面就是关于索引和文档API的使用。

Java REST Client

Java REST Client 有两种风格:

  • Java Low Level REST Client :用于Elasticsearch的官方低级客户端。它允许通过http与Elasticsearch集群通信。将请求编排和响应反编排留给用户自己处理。它兼容所有的Elasticsearch版本。
  • Java High Level REST Client :用于Elasticsearch的官方高级客户端。它是基于低级客户端的,它提供很多API,并负责请求的编排与响应的反编排。
  • 官方更建议我们用Java High Level REST Client,它执行HTTP请求,而不是序列号的Java请求。

下面测试使用的是 Java 高级客户端。

2、索引的API操作详解

创建索引

1
2
3
4
5
6
7
8
9
10
java复制代码    //创建索引测试
@Test
void contextLoads() throws IOException {
//创建索引的请求
CreateIndexRequest request = new CreateIndexRequest("wanli");
//执行创建索引的请求,返回一个响应
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
//打印响应
System.out.println(response); //org.elasticsearch.client.indices.CreateIndexResponse@6ce3e00
}

RestHighLevelClient中的所有API都接受RequestOptions,您可以使用该选项定制请求,而不会改变Elasticsearch执行请求的方式。例如,您可以在此处指定一个节点选择器来控制哪个节点接收请求。

判断索引是否存在

1
2
3
4
5
6
7
java复制代码    //判断索引是否存在,存在返回true,不存在返回false
@Test
public void test1() throws IOException {
GetIndexRequest request = new GetIndexRequest("wanli_index");
boolean b = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(b); //true
}

删除索引

1
2
3
4
5
6
7
java复制代码    //删除索引
@Test
public void test2() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("wanli_index");
AcknowledgedResponse response = client.indices().delete(request,RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged()); //返回true删除成功
}

3、文档的API操作详解

添加文档

实体类:

1
2
3
4
5
6
7
8
java复制代码@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}

添加文档代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码    @Test
public void test3() throws IOException {

User user = new User("万里", 3);

//创建请求
IndexRequest request = new IndexRequest("wanli_index");

request
.id("1") //文档id,不指定会有默认id
.timeout(TimeValue.timeValueSeconds(1))//分片超时时间
.source(JSON.toJSONString(user), XContentType.JSON); //将文档源设置为索引,user对象转化成json字符串

IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.toString()); //IndexResponse[index=wanli_index,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":1,"failed":0}]

System.out.println(response.status());//CREATED 操作状态
}

判断文档是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    //判断文档是否存在
@Test
public void test4() throws IOException {
//针对指定的索引和文档 ID 构造一个新的 get 请求
GetRequest request = new GetRequest("wanli_index","1");

String[] includes = Strings.EMPTY_ARRAY;
String[] excludes = new String[]{"age"};//值获取 _source 中包含age的文档信息
//fetchSourceContext指定需要返回字段的上下文,提供更加完善的过滤逻辑,主要特性为支持include、exclude和支持通篇符过滤。
request.fetchSourceContext(new FetchSourceContext(true, includes, excludes));

boolean b = client.exists(request, RequestOptions.DEFAULT);
System.out.println(b); //true则存在
}

获取文档信息

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    //获取文档信息
@Test
public void test5() throws IOException {
GetRequest request = new GetRequest("wanli_index", "1");

GetResponse response = client.get(request, RequestOptions.DEFAULT);
System.out.println(response.toString());//{"_index":"wanli_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":3,"name":"万里"}}

System.out.println(response.getSource());//获取_source:{name=万里, age=3}
System.out.println(response.getVersion());//1
System.out.println(response);//{"_index":"wanli_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":3,"name":"万里"}}

}

修改文档信息

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    //修改文档信息
@Test
public void test6() throws IOException {
UpdateRequest request = new UpdateRequest("wanli_index", "1");
request.timeout("1s");
User user = new User("神明", 18);

//doc里面填具体的内容和文档格式
request.doc(JSON.toJSONString(user),XContentType.JSON);

UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
System.out.println(response.status()); //返回ok表示操作成功
}

删除文档

1
2
3
4
5
6
7
8
9
java复制代码    //删除文档
@Test
public void test7() throws IOException {

DeleteRequest request = new DeleteRequest("wanli_index", "1");
request.timeout("1s");
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println(response.status());//返回ok表示操作成功
}

批量插入文档

BulkRequest:批量请求包含有序的IndexRequest 、 DeleteRequest 和UpdateRequest ,并允许在单个批处理中执行它。 请注意,我们仅支持批量请求而不是每个项目的刷新。

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
java复制代码   //批量插入文档
@Test
public void test8() throws IOException {
BulkRequest request = new BulkRequest();

request.timeout("10s");

ArrayList<User> list = new ArrayList<>();

list.add(new User("shenming1",3));
list.add(new User("shenming2",3));
list.add(new User("shenming3",3));
list.add(new User("shenming4",3));
list.add(new User("shenming5",3));

for (int i = 0; i < list.size(); i++) {
request.add(
new IndexRequest("wanli_index")
.id(""+(i+1))
.source(JSON.toJSONString(list.get(i)),XContentType.JSON));
}
BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
System.out.println(response.status()); //ok

}

根据具体的需求调用下面的方法。

在这里插入图片描述

复杂查询操作

1、构建查询请求:SearchRequest request = new SearchRequest(“wanli_index”);

2、构建搜索条件:SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource();

3、将查询条件放入请求:request.source(sourceBuilder);

4、执行请求:SearchResponse response = client.search(request, RequestOptions.DEFAULT);

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复制代码@Test
public void test9() throws IOException {
//构建查询请求
SearchRequest request = new SearchRequest("wanli_index");

//构建搜索条件
SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource();

//设置高亮
//HighlightBuilder highlightBuilder = new HighlightBuilder();
//highlightBuilder.field("name");
//highlightBuilder.preTags("<span style='color:red'>");
//highlightBuilder.postTags("</span>");
//searchSourceBuilder.highlighter(highlightBuilder);
//分页:
//sourceBuilder.from(0);
//sourceBuilder.size(10);

//QueryBuilders 工具类实现查询条件的构建
//QueryBuilders.termQuery 精确查询
//QueryBuilders.matchAllQuery() 匹配所有
TermQueryBuilder queryBuilder = QueryBuilders.termQuery("name", "shenming1");
sourceBuilder.query(queryBuilder);//将精确查询条件放入搜索条件
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));//设置超时时间

//将查询条件放入请求
request.source(sourceBuilder);

//执行请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//打印搜索命中文档结果
System.out.println(JSON.toJSONString(response.getHits()));
//{"fragment":true,"hits":[{"fields":{},"fragment":false,"highlightFields":{},"id":"1","matchedQueries":[],"primaryTerm":0,"rawSortValues":[],"score":1.1727202,"seqNo":-2,"sortValues":[],"sourceAsMap":{"name":"shenming1","age":3},"sourceAsString":"{\"age\":3,\"name\":\"shenming1\"}","sourceRef":{"fragment":true},"type":"_doc","version":-1}],"maxScore":1.1727202,"totalHits":{"relation":"EQUAL_TO","value":1}}


System.out.println("====================================");
for (SearchHit searchHit : response.getHits().getHits()) {
System.out.println(searchHit.getSourceAsMap());//{name=shenming1, age=3}
}

}

本文转载自: 掘金

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

1…178179180…956

开发者博客

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