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

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


  • 首页

  • 归档

  • 搜索

SpringCloudAlibaba全网最全讲解5️⃣之Fe

发表于 2021-08-28

🌈专栏简介

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩,这个专栏主要是介绍目前微服务最主流的解决方案,SpringCloudAlibaba,将会分组件介绍。专栏地址: [SpringCloudAlibaba](https://juejin.cn/column/7001291481705086990)。**
  • SpringCloudAlibaba全网最全讲解3️⃣之Ribbon(建议收藏)
  • SpringCloudAlibaba全网最全讲解3️⃣之Nacos(建议收藏)
  • SpringCloudAlibaba全网最全讲解2️⃣(建议收藏)
  • SpringCloudAlibaba全网最全讲解1️⃣(建议收藏)

七、远程调用:Feign

7.1、Feign简介

Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。


Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。

7.2、Feign实战

7.2.1、添加依赖

在shop-order-server项目的pom文件加入Fegin的依赖。
1
2
3
4
5
xml复制代码<!--fegin组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

7.2.2、添加注解

我们需要在启动类上添加@EnableFeignClients注解,只有有了这个注解才会扫描。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 支持Feign
public class ShopOrderServerApp {
public static void main(String[] args) {
SpringApplication.run(ShopOrderServerApp.class,args);
}

@Bean
@LoadBalanced
public RestTemplate getInstance(){
return new RestTemplate();
}
}

7.2.3、新增ProductFeignApi

在Shop-order-server中新增一个接口。
1
2
3
4
5
6
java复制代码// name指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
@FeignClient(name = "product-service")
public interface ProductFeignApi {
@RequestMapping("/product")
Product findById(@RequestParam("productId") Long productId);
}

7.2.4、修改Controller

1
2
3
4
5
6
7
8
9
java复制代码  @Autowired
ProductFeignApi productFeignApi;
@Override
public Order getById(Long oid, Long pid) {
Product product = productFeignApi.findById(pid);
Order order = orderDao.getOne(oid);
order.setPname(product.getPname());
return order;
}

7.3、Feign的重要属性

我们可以配置超时属性。
1
2
3
4
5
6
yaml复制代码feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000

7.4、Feign实现原理

  1. 启动后会会根据配置在启动类上的@SpringBootApplication去扫描贴了@FeignClient注解的类,并为其创建代理对象。
  2. 通过反射拿到代理类实现的接口:ProductFeignApi。
  3. 通过反射拿到接口上注解并且将注解中心的name属性拿出来:product-service。
  4. 通过反射拿到接口中的方法,并且拿到接口中方法上的注解@RequestMapping,并且把值拿出来:/product。
  5. 将方法中参数注解中的值也拿出来,这个是我们传进来的参数:productId。
  6. 拼接出路径:http://product-service/product?productId=1。
  7. 根据本地的服务清单去找对应的节点信息。
  8. 根据你配置的ribbon负载均衡策略去选择节点。
  9. 将product-service替换成对应的节点信息和端口。
  10. 使用RestTemplate发送请求。

本文转载自: 掘金

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

Flink 从0-1实现 电商实时数仓 - DIM & DW

发表于 2021-08-28

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

往期:juejin.cn/column/6994…

12. 处理业务数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
ini复制代码public class BaseDBApp {

/**
* 业务数据 topic
*/
private static final String TOPIC_BASE = "ods_base_db_m";
private static final String BASE_GROUP_ID = "ods_dwd_base_log_db";

public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);

//检查点,省略

//拿到数据流
DataStreamSource<String> dataStreamSource = env.addSource(KafkaUtil.ofSource(TOPIC_BASE, BASE_GROUP_ID));
//转化格式,ETL
SingleOutputStreamOperator<JSONObject> filteredDs = dataStreamSource
.map(JSON::parseObject)
.filter(new RichFilterFunction<JSONObject>() {
@Override
public boolean filter(JSONObject jsonObject) throws Exception {
return StringUtils.isNotBlank(jsonObject.getString("table"))
&& jsonObject.getJSONObject("data") != null;
}
});
// Flink CDC 读取配置流
DataStreamSource<String> ruleSource = env.addSource(MySQLSource.<String>builder()
.hostname("hadoop3")
.port(3306)
.username("root")
.password("密码")
.databaseList("tmall_realtime")
.tableList("tmall_realtime.table_process")
.deserializer(new DebeziumDeserializationSchema<String>() {
@Override
public void deserialize(SourceRecord sourceRecord, Collector<String> collector) throws Exception {
Struct value = (Struct) sourceRecord.value();
/*
* Struct{
* after=Struct{
* source_table=111,
* operate_type=tses,
* sink_type=1,
* sink_table=1111
* },
* source=Struct{
* db=tmall_realtime,
* table=table_process
* },
* op=c
* }
*/
Struct source = value.getStruct("source");

JSONObject jsonObject = new JSONObject();
jsonObject.put("database", source.getString("db"));
jsonObject.put("table", source.getString("table"));
jsonObject.put("type", CDCTypeEnum.of(value.getString("op")).toString().toLowerCase());
Struct after = value.getStruct("after");
JSONObject data = new JSONObject();
for (Field field : after.schema().fields()) {
data.put(field.name(), after.get(field.name()));
}
jsonObject.put("data", data);

collector.collect(jsonObject.toJSONString());
}

@Override
public TypeInformation<String> getProducedType() {
return BasicTypeInfo.STRING_TYPE_INFO;
}
})
.startupOptions(StartupOptions.initial())
.build()
);

//配置流状态
MapStateDescriptor<String, TableProcess> mapStateDescriptor = new MapStateDescriptor<>("table_process", String.class, TableProcess.class);

//广播配置流状态
BroadcastStream<String> broadcast = ruleSource.broadcast(mapStateDescriptor);

//定义侧输出流,存放DIM数据
OutputTag<JSONObject> dimTag = new OutputTag<JSONObject>("dim_tag") {
};

//正式处理数据
SingleOutputStreamOperator<JSONObject> dwdDs = filteredDs
//合并 配置流
.connect(broadcast)
.process(new BroadcastProcessFunction<JSONObject, String, JSONObject>() {

private Connection connection;

@Override
public void open(Configuration parameters) throws Exception {
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
connection = DriverManager.getConnection(TmallConfig.PHOENIX_SERVER);
}

@Override
public void close() throws Exception {
connection.close();
}

/**
* 处理 ODS 数据流
* @param jsonObject
* @param readOnlyContext
* @param collector
* @throws Exception
*/
@Override
public void processElement(JSONObject jsonObject, ReadOnlyContext readOnlyContext, Collector<JSONObject> collector) throws Exception {
//获取 配置流状态
ReadOnlyBroadcastState<String, TableProcess> broadcastState = readOnlyContext.getBroadcastState(mapStateDescriptor);

String table = jsonObject.getString("table");
String type = jsonObject.getString("type");
// MaxWell 处理历史数据 insert 的操作类型是 bootstrap-insert 需要修正一些
if ("bootstrap-insert".equals(type)) {
type = "insert";
jsonObject.put("type", type);
}

//拿到配置
String key = table + ":" + type;
TableProcess tableProcess = broadcastState.get(key);

if (tableProcess != null) {
//目标表放进去
jsonObject.put("sink_table", tableProcess.getSinkTable());
jsonObject.put("sink_pk", tableProcess.getSinkPk());
//过滤字段
HashSet<String> columnSet = Sets.newHashSet(tableProcess.getSinkColumns().split(","));
jsonObject.getJSONObject("data").entrySet().removeIf(e -> !columnSet.contains(e.getKey()));
//发送位置
String sinkType = tableProcess.getSinkType();
if (TableProcess.SINK_TYPE_KAFKA.equals(sinkType)) {
collector.collect(jsonObject);
} else if (TableProcess.SINK_TYPE_HBASE.equals(sinkType)) {
readOnlyContext.output(dimTag, jsonObject);
}
} else {
//没有配置
System.out.println("NO this Key in TableProcess" + key);
}
}

/**
* 处理 配置流 数据
* @param s
* @param context
* @param collector
* @throws Exception
*/
@Override
public void processBroadcastElement(String s, Context context, Collector<JSONObject> collector) throws Exception {
JSONObject jsonObject = JSON.parseObject(s);
TableProcess tableProcess = jsonObject.getObject("data", TableProcess.class);
String sourceTable = tableProcess.getSourceTable();
String operateType = tableProcess.getOperateType();
String sinkType = tableProcess.getSinkType();
String sinkPk = StringUtils.defaultString(tableProcess.getSinkPk(), "id");
String sinkExt = StringUtils.defaultString(tableProcess.getSinkExtend());
String sinkTable = tableProcess.getSinkTable();
String sinkColumns = tableProcess.getSinkColumns();

//如果是维度数据,需要通过Phoenix创建表
if (TableProcess.SINK_TYPE_HBASE.equals(sinkType) && CDCTypeEnum.INSERT.toString().toLowerCase().equals(operateType)) {
StringBuilder sql = new StringBuilder();
sql.append("create table if not exists ").append(TmallConfig.HBASE_SCHEMA).append(".").append(sinkTable).append(" ( ");
String[] columns = sinkColumns.split(",");
for (int i = 0; i < columns.length; i++) {
String column = columns[i];
if (sinkPk.equals(column)) {
sql.append(column).append(" varchar primary key ");
} else {
sql.append("info.").append(column).append(" varchar ");
}
if (i < columns.length - 1) {
sql.append(" , ");
}
}
sql.append(" ) ")
.append(sinkExt);
System.out.println(sql);
try (PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
preparedStatement.execute();
}
}

//写入状态进行广播
BroadcastState<String, TableProcess> broadcastState = context.getBroadcastState(mapStateDescriptor);
broadcastState.put(sourceTable + ":" + operateType, tableProcess);
}
});

//处理 DIM 侧输出流 存入 HBase
dwdDs.getSideOutput(dimTag).addSink(new RichSinkFunction<JSONObject>() {

private Connection connection;

@Override
public void open(Configuration parameters) throws Exception {
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
connection = DriverManager.getConnection(TmallConfig.PHOENIX_SERVER);
}

@Override
public void close() throws Exception {
connection.close();
}

@Override
public void invoke(JSONObject value, Context context) throws Exception {
JSONObject data = value.getJSONObject("data");
StringBuilder sql = new StringBuilder();
//目标表名
String sinkTable = value.getString("sink_table");
sql.append("upsert into ").append(TmallConfig.HBASE_SCHEMA).append(".").append(sinkTable).append(" (")
.append(StringUtils.join(data.keySet(), ","))
.append(") ")
.append("values( '")
.append(StringUtils.join(data.values(), "','"))
.append("' ) ");
//入库
try (PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
preparedStatement.execute();
// 默认不自动提交,需要手动提交
connection.commit();
}
//删除缓存
String type = value.getString("type");
if ("update".equals(type) || "delete".equals(type)) {
String sinkPk = value.getString("sink_pk");
DimUtil.delDimCache(sinkTable, data.getString(sinkPk));
}
}
});

// 处理 DWD 主流数据,存入 Kafka
dwdDs.addSink(KafkaUtil.ofSink(new KafkaSerializationSchema<JSONObject>() {
@Override
public ProducerRecord<byte[], byte[]> serialize(JSONObject jsonObject, @Nullable Long aLong) {
return new ProducerRecord<>(jsonObject.getString("sink_table"), jsonObject.getJSONObject("data").toJSONString().getBytes(StandardCharsets.UTF_8));
}
}));

//执行
env.execute("db_ods_to_dwd");
}
}
13. 处理日志数据
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
java复制代码public class BaseLogApp {

/**
* 所以日志数据
*/
private static final String TOPIC_BASE = "ods_base_log";
private static final String BASE_GROUP_ID = "ods_dwd_base_log_app";

/**
* 启动日志数据
*/
private static final String TOPIC_START = "dwd_start_log";
/**
* 页面日志数据
*/
private static final String TOPIC_PAGE = "dwd_page_log";
/**
* 曝光日志数据
*/
private static final String TOPIC_DISPLAY = "dwd_display_log";

public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);

//省略检查点

//拿到数据流
DataStreamSource<String> dataStreamSource = env.addSource(KafkaUtil.ofSource(TOPIC_BASE, BASE_GROUP_ID));

// 处理新用户字段,防止前端数据错误
SingleOutputStreamOperator<JSONObject> midWithNewFlagDS = dataStreamSource
.map(JSON::parseObject)
.keyBy(j -> j.getJSONObject("common").getString("mid"))
.map(new RichMapFunction<JSONObject, JSONObject>() {

private ValueState<String> newMidDateState;
private SimpleDateFormat yyyyMMdd;

@Override
public void open(Configuration parameters) throws Exception {
newMidDateState = getRuntimeContext().getState(new ValueStateDescriptor<String>("newMidDateState", String.class));
yyyyMMdd = new SimpleDateFormat("yyyyMMdd");
}

@Override
public JSONObject map(JSONObject jsonObject) throws Exception {
String isNew = jsonObject.getJSONObject("common").getString("is_new");
//1判断是不是新用户,是新用户,修复
if ("1".equals(isNew)) {
String newMidDate = newMidDateState.value();
String ts = yyyyMMdd.format(new Date(jsonObject.getLong("ts")));
if (StringUtils.isEmpty(newMidDate)) {
newMidDateState.update(ts);
} else {
if (!newMidDate.equals(ts)) {
jsonObject.getJSONObject("common").put("is_new", "0");
}
}
}
return jsonObject;
}
});

//启动日志侧输出流
OutputTag<String> startTag = new OutputTag<String>("startTag") {
};
//曝光日志侧输出流
OutputTag<String> displayTag = new OutputTag<String>("displayTag") {
};

// 判断 不同的日志类型 输出到各流
SingleOutputStreamOperator<String> pageDStream = midWithNewFlagDS
.process(new ProcessFunction<JSONObject, String>() {
@Override
public void processElement(JSONObject jsonObject, Context context, Collector<String> collector) throws Exception {
JSONObject start = jsonObject.getJSONObject("start");
if (start != null) {
context.output(startTag, jsonObject.toJSONString());
} else {
collector.collect(jsonObject.toJSONString());
JSONArray displays = jsonObject.getJSONArray("displays");
if (!CollectionUtil.isNullOrEmpty(displays)) {
for (int i = 0; i < displays.size(); i++) {
JSONObject displaysJsonObject = displays.getJSONObject(i);
String pageId = jsonObject.getJSONObject("page").getString("page_id");
displaysJsonObject.put("page_id", pageId);
context.output(displayTag, displaysJsonObject.toJSONString());
}
}
}
}
});

//写入 kafka
pageDStream.addSink(KafkaUtil.ofSink(TOPIC_PAGE));
pageDStream.getSideOutput(startTag).addSink(KafkaUtil.ofSink(TOPIC_START));
pageDStream.getSideOutput(displayTag).addSink(KafkaUtil.ofSink(TOPIC_DISPLAY));

env.execute("log_ods_to_dwd");
}

}

下期预告:DWM层

关注专栏持续更新 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

本文转载自: 掘金

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

Docker系列之Dockerfile详解ENV ARG V

发表于 2021-08-28

本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

ENV 设置环境变量

1、格式

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

2、作用

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

1
2
ini复制代码ENV VERSION=1.0 DEBUG=on \
  NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:

1
2
3
4
5
6
7
8
9
bash复制代码ENV NODE_VERSION 7.2.0
​
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

下列指令可以支持环境变量展开: ADD、COPY、ENV、EXPOSE、FROM、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD、RUN。

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

ARG 构建参数

1、格式

  • ARG <参数名>[=<默认值>]

2、作用

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。

VOLUME 定义匿名卷

1、格式

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

2、作用

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

1
bash复制代码VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

1
arduino复制代码docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

本文转载自: 掘金

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

基于Spring+SpringMVC+MyBatis的汽车租

发表于 2021-08-27

🌈往期回顾

大家好,我是XiaoLin,我在工作的时候帮助过无数的大学生开发过毕业设计和课程设计的项目,现在全部展示出来,专栏地址:100高级个项目合集。这是1第一个项目。

序言

在这里插入图片描述

又快到了一年一度的课程设计的日子了,今天我给大家带来了一个应对毕设、课程设计的精品项目,安装部署学习一条龙服务。

技术总结

在这里插入图片描述

本次项目使用的技术点SSM、MySQL、JQuery、layui、echarts等相关技术,技术栈还是很全面的。主要的功能大致有:
  1. 客户管理
  2. 汽车租车还车
  3. 车辆管理
  4. 汽车入库
  5. 系统管理
  6. 统计分析
  7. 等等等

系统截图

img

img

img

img

img

img

img

img

img

img

img

论文截图

image-20210527153556386

足足1.5w字,而且**查过重!查过重!查过重!**

在这里插入图片描述

项目源码

目前该项目已经在我的Gitee[仓库](https://gitee.com/XiaoLin_Java/ssmcar-rental),后期会陆续更新其他项目。

img

本文转载自: 掘金

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

postman使用教程 1、postman是什么 2、pos

发表于 2021-08-27

1、postman是什么

postman是常见的接口测试工具,主要用于http接口调试,如何安装,直接百度即可安装

2、postman有哪些功能

  • 调试http接口
  • 导出为其他语言代码
  • 创建集合并保存相应的接口请求
  • postman抓包,并代理发送请求
  • postman开发脚本
  • postman设计自动化测试用例

3、postman和http接口结合

postman主要用途是发送http请求,那我们来介绍下什么是http,http报文结构是什么样子的

3.1、什么是http

http(Hyper Text Transfer Protocol,HTTP)是一种协议,叫做超文本传输协议,属于c/s或者b/s架构,即客户端去请求服务端,拿到数据后进行展示
image.png
tcp/ip 4层模型

image.png

那么http和https的区别是什么呢,https是在http的基础上加了ssl加密,也就是传输的http报文都是经过ssl加密的

*常见面试题: tcp/ip 4层模型和对应的产品有哪些?

3.2、http报文内容

HTTP报文结构内容

序号 报文内容 作用
1 url(统一资源定位符) 告诉浏览器资源在哪,有一定的历史渊源,最开始http主要用于各个美国欧洲的实验室存储文档,xxx.com/meiguo.html现在的作用就是一个接口地址, 比如www.taobao.com/add/user,这是…
2 方法(method) GET,POST,PUT,DELETE,OPTIONS,常用的还是GET和POST
3 params 只要get请求有,get请求去获取资源的时候,比如去百度搜索的时候,输入1个参数查询
4 data 只有post请求有,post去提交数据的时候,会把数据放到data里发送给服务端,类型比较多,拆开讲解
5 header http头信息,这里面包含的数据很多,拆分讲解

HTTP post接口data结构内容

序号 内容 作用
1 json数据 通过json把数据传给服务端
2 Form-data 通过form表单把数据传给服务端
3 mutilpart-binary 通过二进制,主要是传文件的时候使用

HTTP header内容:通过百度抓包分析

序号 内容 作用
1 cookies 用户权限认证,通过cookies来证明你是你,比如证明你是你爸的儿子cookies:”father=ligang”
2 connection Connection:keep-alive发送请求后是否断开tcp连接,keep-alive是不断开,close代表断开请求,获取到资源后立马断开请求
3 host 一般就是域名,一个服务器上部署了多个服务,尤其在以php语言开发的web服务,host如果不带就会出现异常
4 User-Agent 用户代理,主要用于判断你是爬虫请求,还是正常用户请求,还是移动端请求
5 content-type 主要是在post请求的时候,如果是json数据,content-type必须是application/json,如果是form-data必须是mutilpart-formdata,如果是二进制,必须是mutilpart-binary

3.3、使用postman实战调试

3.3.1、get接口调试, 查询全国各地的天气

1
2
bash复制代码接口地址: http://ip:5000/simpleWeather/get
请求方法: GET

​ 请求参数说明:

参数名称 是否必填 类型 说明
city 是 String 城市名称,查询该城市天气

​ 通过postman配置请求:

image.png

​ 图中1的位置表示请求类型是GET还是POST,图中2的位置表示请求的url或者叫接口路径,图中3的位置表示GET请求参数params,只需要传递1个参数city

3.3.2、post接口通过application/json发送数据

1
2
3
bash复制代码接口地址: http://ip:5000/add/user
请求方法: POST
数据类型: application/json

​ 请求参数说明

参数名称 是否必填 类型 说明
name 是 String 名字
age 是 int 年龄
status 是 String 感情状态,单身,非单身
contact 是 String 微信号或者手机号

​ 通过postman构造发送请求

image.png

​ 1.请求为POST请求

​ 2.配置url

​ 3.参数在body里

​ 4.类型选择JSON

​ 5.设置的json数据

1
bash复制代码可以通过http://ip:5000/get/user获取到所有的数据

3.3.3、post接口通过form-data发送数据

1
2
3
makefile复制代码请求地址: http://ip:5000/mobile/serach
请求类型: POST
数据类型: form-data

请求参数说明

参数名称 是否必填 类型 说明
mobile 是 String 手机号

使用postman构造form-data的请求

image.png

​ 1.接口请求方法类型为POST

​ 2.设置的接口地址

​ 3.数据类型设置为form-data,传递的参数有mobile手机号

3.3.4、post接口通过form-data上传文件

1
2
3
makefile复制代码请求地址: http://ip:5000/upload
请求类型: POST
数据类型: form-data

​ 请求参数说明

参数名称 是否必填 类型 说明
image 是 二进制文件 需要上传的文件

​ 使用postman构造请求

image.png

​ 1.表示请求方式是POST

​ 2.设置的接口为/upload

​ 3.设置image参数为benchi.jpeg的二进制文件

上传完成后可以通过接口查看数据是否存在

1
bash复制代码http://ip:5000/file/get?fileName=benchi.jpeg

4、postman导出功能

1.使用postman的右上角可以导出为其他语言的代码

image.png

2.选择生成python代码

image.png

3.拷贝代码出来后通过pycharm运行,可以正常运行

5、创建集合并保存响应的接口请求

1.点击左上角的Collection,切换到集合tab页下面
image.png

2.新建集合
image.png

3.输入集合名字
image.png

4.保存接口到集合中
image.png

5.把以上的几个接口都保存到集合中
image.png

6、postman抓包

1.在右上角开启抓包

image.png

2.在客户端配置代理端口号

3.在客户端发送请求

4.postman查看抓包数据
image.png

7、postman开发脚本

1.配置全局变量

1
js复制代码postman.setEnvironmentVariable("city", "上海")

image.png

2.配置test用例脚本

1
2
3
ini复制代码tests["status is 200"] = responseCode.code === 200
var jsonData = JSON.parse(responseBody);
tests["city is right"] = jsonData.result.city === postman.getEnvironmentVariable("city")

image.png

本文转载自: 掘金

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

SpringDataElasticSearch框架

发表于 2021-08-27

SpringDataElasticSearch框架

依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.10.2</elasticsearch.version>
</properties>
配置yml
1
2
3
4
5
yml复制代码spring:
elasticsearch:
rest:
uris:
- xxx.xxx.xxx.xxx:9200
配置实体类和Document的对应关系
  1. @Document(indexName = “cube_goods”, shards = 5, replicas = 1)//配置对应的索引库、分片数、副本数
  2. @Id //指定id
  3. @Field(index = true, store = true, type = FieldType.Text, analyzer = “ik_smart”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
@Data
@Document(indexName = "blog_1", shards = 5, replicas = 1)
public class Blog {
@Id
@Field(type = FieldType.Long)
private long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", store = true)
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content;
@Field(type = FieldType.Text, analyzer = "ik_max_word", store = true)
private String comment;
@Field(type = FieldType.Keyword, store = true)
private String mobile;
}
文档的管理ElasticSearchRepository接口
使用方法

创建一个接口,继承与ElasticSearchRepository,指定使用的Entity类及对应主键数据类型,Springboot自动扫描接口并创建代理对象

  1. 新增、更新数据
    • 使用repository的save方法实现
  2. 删除数据
    • deleteById
    • deleteAll
  3. 查询数据
    • 可以使用repository自带的查询方法
      • findById
      • findAll
    • 可以自定义查询方法
      • findBy{Titlte}And{contnet}(String title, String content); 按照命名规则定义方法,就可以实现相应的查询
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复制代码public interface BlogRepostory extends ElasticsearchRepository<Blog, Long> {
//按照命名规则定义方法
List<Blog> findByTitle(String title);
List<Blog> findByContent(String content, Pageable page);
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class BlogRepostoryTest {
@Autowired
private BlogRepostory repostory;

@Test
public void testFindByTitle() {
List<Blog> blogs = repostory.findByTitle("电影");
blogs.forEach(System.out::println);
}

@Test
public void testFindByPage() {
List<Blog> list = repostory.findByContent("电影", PageRequest.of(0, 5));
list.forEach(e-> System.out.println(e));
}

}
使用ElasticSearchRestTemplate对象(复杂查询使用这个)
1)创建索引库

​ template.indexOps(IndexCoordinates.of(“mytest”)).create();

2)设置mapping信息

​ 需要创建一个实体类,其中配置实体类和文档的映射关系,使用注解配置

​ 可以从Entity中生成mapping信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class ElasticSearchTemplateTest {
@Autowired
private ElasticsearchRestTemplate template;

@Test
public void createIndex() {
//template.indexOps(Blog.class).create();//从实体类中创建
template.indexOps(IndexCoordinates.of("hello_1")).create();//指定名称创建
}

@Test
public void putMappiing() {
Document mapping = template.indexOps(Blog.class).createMapping();//从实体类中创建
template.indexOps(Blog.class).putMapping(mapping);
}
}
3)查询
  1. 封装查询条件 NativeSearchQuery
  2. 执行查询 SearchHits searchHits = template.search(query, Blog.class);
  3. 结果处理
* 查询到的记录数 **searchHits.getTotalHits();**
* 查询的记录结果集 **List<SearchHit> searchHits1 = searchHits.getSearchHits();** 然后遍历


    + 查询的记录 **e.getContent()**
    + 高亮结果 **Map<String, List> highlightFields = e.getHighlightFields();**


    ​ **List title = highlightFields.get("title");**
    + 聚合结果 **Map<String, Aggregation> stringAggregationMap = searchHits.getAggregations().asMap();**


    ​ **Aggregation aggregation = stringAggregationMap.get("mobile\_count");**


    ​ **ParsedValueCount valueCount = searchHits.getAggregations().get("mobile\_count");**


    ​ **System.out.println(aggregation.getName());**
    ​ **System.out.println(valueCount.getValue());**
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
java复制代码
@Test
public void testQuery() {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.termQuery("title", "电影"))
.withHighlightBuilder(new HighlightBuilder().field("title").preTags("<em>").postTags("</em>"))
.withFilter(QueryBuilders.termQuery("title", "湖南"))
.addAggregation(AggregationBuilders.count("mobile_count").field("mobile"))
.build();
SearchHits<Blog> searchHits = template.search(query, Blog.class);
long totalHits = searchHits.getTotalHits();
System.out.println(totalHits);
List<SearchHit<Blog>> searchHits1 = searchHits.getSearchHits();
searchHits1.forEach(e->{
Blog content = e.getContent();
System.out.println(content);
Map<String, List<String>> highlightFields = e.getHighlightFields();
List<String> title = highlightFields.get("title");
System.out.println(title);
});
Map<String, Aggregation> stringAggregationMap = searchHits.getAggregations().asMap();
System.out.println(stringAggregationMap);
Aggregation aggregation = stringAggregationMap.get("mobile_count");
ParsedValueCount valueCount = searchHits.getAggregations().get("mobile_count");
System.out.println(aggregation == valueCount);
System.out.println(aggregation.getName());
System.out.println(valueCount.getValue());
}

本文转载自: 掘金

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

聊一下MySQL的慢SQL优化方向

发表于 2021-08-27

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

影响一个系统的运行速度的原因有很多,是多方面的,甚至可能是偶然性的,或前端,或后端,或数据库,或中间件,或服务器,或网络等等等等,真正的去定位一个问题需要对系统有一定的认知,可以根据自身的判断去缩小问题范围。

今天不说其他的优化,单独把数据库的优化拿出来说几个优化方向。

跟系统的优化方向一样,数据库的优化,同样也是多方面的,其中涵盖着SQL语句的执行情况,数据库自身的情况等等,下面我们就来说一下MySQL数据库中的慢SQL语句优化方向,希望也能给到大家一些优化思路。

SQL语句优化

SQL语句的优化,有很多文章说起,也有很多在SQL编写上的指导;但是那种只能支持基本开发,如果要排查问题,那就不能单单的只是停留在SQL编写上了,而是有一个整体的发现问题的流程。

本次优化方向,大概分为发现慢查询SQL,查看并解析SQL执行计划,SQL编写上的优化,索引优化等几个方面。

记录慢查询SQL

MySQL中记录慢查询SQL是可以利用MySQL内部配置来实现的,这个配置就是slow_query_log配置。

可利用show variables like ‘%query%’;查询出以下三个相关结果。

1
2
3
sql复制代码long_query_time     | 1.00000
slow_query_log | off
slow_query_log_file | /data/mysql/mysql_slow.log

解释一下这三个参数,

long_query_time:如何区分SQL查询是慢查询,就要规定一个查询时间,超过这个时间的就归类于慢查询,此参数就是来设置时间范围的;以秒为单位,可以设置小数。

slow_query_log:此参数为是否开启记录慢查询SQL的开关,两个选择,on或者off,默认为off,所以在这里我们就知道如果要开启慢查询SQL记录,需要手动设置开启。

slow_query_log_file:慢查询SQL日志的文件路径,可以自行指定。

如何修改配置

有两个方法。

其一:修改my.ini或者是my.cnf文件,将此三项配置进行一个配置。

其二:直接在sqlplus中,使用set语法来修改参数,但是重启mysql数据库后就会失效,sql如下:

1
2
3
4
5
sql复制代码set global long_query_time = 10;

set global slow_query_log = on;

set global slow_query_log_file = /data/mysql/mysql_slow.log;

因为这个方法会重启失效,所以还是建议使用第一种方式。

查看慢查询日志

如何查询慢查询日志呢,如果量很小的情况下,其实是不需要使用工具的,完全可以直接打开即可。

如果量比较大,就需要mysqldumpslow工具查询会更方便。

mysqldumpslow是和mysqld相同类型的执行脚本,可以直接在命令行中执行,具体的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
text复制代码mysqldumpslow参数:

-s,是order的顺序
-----al 平均锁定时间
-----ar 平均返回记录时间
-----at 平均查询时间(默认)
-----c 计数
-----l 锁定时间
-----r 返回记录
-----t 查询时间

-t,top,即为返回前面多少条的数据
-g,自定义正则表达式

举个例子,如下:

mysqldumpslow -s r -t 5 /data/mysql/mysql_slow.log

查询出返回记录集最多的5个慢查询SQL。

更多用法之后我建个测试库单独写篇文章细说一下。

查看SQL执行计划

查看执行计划关键词:EXPLAIN

如何使用

就是直接执行 EXPLAIN SELECT * FROM TABLE_NAME;

这个一开始我是打算简单说一下的,后来发现篇幅太长了,这个留待下篇文章里,感谢理解。

SQL编写优化

SQL的编写优化就很多了,我这里也整理出了一些,请大家自行查漏补缺。

  1. 查询语句无论是使用哪种判断条件 等于、小于、大于, where左侧的条件查询字段不要使用函数或者表达式。
  2. 不要直接使用select *,而应该使用具体需要查询的表字段;select * 使用的是全表扫描,不会走索引的。
  3. 避免在 WHERE 字句中对字段进行 NULL 判断。
  4. 避免在 WHERE 中使用 != 或 <> 操作符。
  5. 使 用 BETWEEN AND 替代 IN。
  6. 为常用搜索条件创建索引
  7. 选择正确的存储引擎, InnoDB 、MyISAM 、MEMORY 等,不同的场景下使用不同的存储引擎会有更好的效果。
  8. 使用 like %123% 不会走索引, 而使用 like 123% 会走索引。非常重要!!!
  9. 选择合适的字段类型。
  10. 设计字段时,要尽量使用NOT NULL。

总结

这里面远远还没有讲全,还有很多种编写规则,同时还有索引的建立并没有聊,留给大家一些自己看书的时间,希望大家有所进步。

本文转载自: 掘金

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

对象不使用时,为什么要赋值为null?

发表于 2021-08-27

前言

许多Java开发者都曾听说过“不使用的对象应手动赋值为null“这句话,而且好多开发者一直信奉着这句话;问其原因,大都是回答“有利于GC更早回收内存,减少内存占用”,但再往深入问就回答不出来了。

鉴于网上有太多关于此问题的误导,本文将通过实例,深入JVM剖析“对象不再使用时赋值为null”这一操作存在的意义,供君参考。

本文尽量不使用专业术语,但仍需要你对JVM有一些概念。

示例代码

我们来看看一段非常简单的代码:

1
2
3
4
5
6
7
arduino复制代码public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    System.gc();
}

我们在if中实例化了一个数组placeHolder,然后在if的作用域外通过System.gc();手动触发了GC,其用意是回收placeHolder,因为placeHolder已经无法访问到了。

来看看输出:

1
2
3
csharp复制代码65536
[GC 68239K->65952K(125952K), 0.0014820 secs]
[Full GC 65952K->65881K(125952K), 0.0093860 secs]

Full GC 65952K->65881K(125952K)代表的意思是:本次GC后,内存占用从65952K降到了65881K。意思其实是说GC没有将placeHolder回收掉,是不是不可思议?

下面来看看遵循“不使用的对象应手动赋值为null“的情况:

1
2
3
4
5
6
7
8
csharp复制代码public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
        placeHolder = null;
    }
    System.gc();
}

其输出为:

1
2
3
csharp复制代码65536
[GC 68239K->65952K(125952K), 0.0014910 secs]
[Full GC 65952K->345K(125952K), 0.0099610 secs]

这次GC后内存占用下降到了345K,即placeHolder被成功回收了!对比两段代码,仅仅将placeHolder赋值为null就解决了GC的问题,真应该感谢“不使用的对象应手动赋值为null“。

等等,为什么例子里placeHolder不赋值为null,GC就“发现不了”placeHolder该回收呢?这才是问题的关键所在。

运行时栈

典型的运行时栈

如果你了解过编译原理,或者程序执行的底层机制,你会知道方法在执行的时候,方法里的变量(局部变量)都是分配在栈上的;当然,对于Java来说,new出来的对象是在堆中,但栈中也会有这个对象的指针,和int一样。

比如对于下面这段代码:

1
2
3
4
5
ini复制代码public static void main(String[] args) {
    int a = 1;
    int b = 2;
    int c = a + b;
}

其运行时栈的状态可以理解成:

索引 变量
1 a
2 b
3 c

“索引”表示变量在栈中的序号,根据方法内代码执行的先后顺序,变量被按顺序放在栈中。

再比如:

1
2
3
4
5
6
7
8
ini复制代码public static void main(String[] args) {
    if (true) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
    int d = 4;
}

这时运行时栈就是:

索引 变量
1 a
2 b
3 c
4 d

容易理解吧?

其实仔细想想上面这个例子的运行时栈是有优化空间的。

Java的栈优化

上面的例子,main()方法运行时占用了4个栈索引空间,但实际上不需要占用这么多。当if执行完后,变量a、b和c都不可能再访问到了,所以它们占用的1~3的栈索引是可以“回收”掉的,比如像这样:

索引 变量
1 a
2 b
3 c
1 d

变量d重用了变量a的栈索引,这样就节约了内存空间。

提醒

上面的“运行时栈”和“索引”是为方便引入而故意发明的词,实际上在JVM中,它们的名字分别叫做“局部变量表”和“Slot”。而且局部变量表在编译时即已确定,不需要等到“运行时”。

GC一瞥

这里来简单讲讲主流GC里非常简单的一小块:如何确定对象可以被回收。另一种表达是,如何确定对象是存活的。

仔细想想,Java的世界中,对象与对象之间是存在关联的,我们可以从一个对象访问到另一个对象。如图所示。

图片

再仔细想想,这些对象与对象之间构成的引用关系,就像是一张大大的图;更清楚一点,是众多的树。

如果我们找到了所有的树根,那么从树根走下去就能找到所有存活的对象,那么那些没有找到的对象,就是已经死亡的了!这样GC就可以把那些对象回收掉了。

现在的问题是,怎么找到树根呢?JVM早有规定,其中一个就是:栈中引用的对象。也就是说,只要堆中的这个对象,在栈中还存在引用,就会被认定是存活的。

提醒

上面介绍的确定对象可以被回收的算法,其名字是“可达性分析算法”。

JVM的“bug”

我们再来回头看看最开始的例子:

1
2
3
4
5
6
7
arduino复制代码public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    System.gc();
}

看看其运行时栈:

1
2
3
4
vbnet复制代码LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      21     0  args   [Ljava/lang/String;
    5      12     1 placeHolder   [B

栈中第一个索引是方法传入参数args,其类型为String[];第二个索引是placeHolder,其类型为byte[]。

联系前面的内容,我们推断placeHolder没有被回收的原因:System.gc();触发GC时,main()方法的运行时栈中,还存在有对args和placeHolder的引用,GC判断这两个对象都是存活的,不进行回收。

也就是说,代码在离开if后,虽然已经离开了placeHolder的作用域,但在此之后,没有任何对运行时栈的读写,placeHolder所在的索引还没有被其他变量重用,所以GC判断其为存活。

为了验证这一推断,我们在System.gc();之前再声明一个变量,按照之前提到的“Java的栈优化”,这个变量会重用placeHolder的索引。

1
2
3
4
5
6
7
8
arduino复制代码public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    int replacer = 1;
    System.gc();
}

看看其运行时栈:

1
2
3
4
5
vbnet复制代码LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      23     0  args   [Ljava/lang/String;
    5      12     1 placeHolder   [B
   19       4     1 replacer   I

不出所料,replacer重用了placeHolder的索引。来看看GC情况:

1
2
3
csharp复制代码65536
[GC 68239K->65984K(125952K), 0.0011620 secs]
[Full GC 65984K->345K(125952K), 0.0095220 secs]

placeHolder被成功回收了!我们的推断也被验证了。

再从运行时栈来看,加上int replacer = 1;和将placeHolder赋值为null起到了同样的作用:断开堆中placeHolder和栈的联系,让GC判断placeHolder已经死亡。

现在算是理清了“不使用的对象应手动赋值为null“的原理了,一切根源都是来自于JVM的一个“bug”:代码离开变量作用域时,并不会自动切断其与堆的联系。为什么这个“bug”一直存在?你不觉得出现这种情况的概率太小了么?算是一个tradeoff了。

总结

希望看到这里你已经明白了“不使用的对象应手动赋值为null“这句话背后的奥义。

我比较赞同《深入理解Java虚拟机》作者的观点:在需要“不使用的对象应手动赋值为null“时大胆去用,但不应当对其有过多依赖,更不能当作是一个普遍规则来推广。

本文转载自: 掘金

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

实战小技巧14:配置文件Properties

发表于 2021-08-27

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

每天一个实战小技巧,Properties配置文件

properties配置文件,相信各位小伙伴都不会太陌生,常用Spring的可能会经常看到它,虽说现在更推荐的是使用Yaml配置文件,但是properties配置文件的使用频率也不低

在jdk中有一个直接关连的类Properties,接下来我们来看一下它的用法

1. 配置文件

properties文件的格式比较简单

  • key = value: 等号左边的为配置key,右边的为配置value(value值会去除前后的空格)
  • #:以#来区分注释

一个基础的配置文件如下

1
2
3
4
5
ini复制代码# 测试
key = value
user.name = 一灰灰blog
user.age = 18
user.skill = java,python,js,shell

2. 配置文件加载

对于Properties配置文件,我们可以非常简单的借助Properties类,来实现配置的加载

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

/**
* 从文件中读取配置
*
* @param propertyFile
* @return
* @throws IOException
*/
public static Properties loadProperties(String propertyFile) throws IOException {
Properties config = new Properties();
config.load(PropertiesUtil.class.getClassLoader().getResourceAsStream(propertyFile));
return config;
}
}

直接使用Properties#config就可以读取配置文件内容,并赋值到java对象

重点注意:

重点看一下Properties类的继承关系,它的父类是Hashtable, 也就是说它的本质是Map对象

1
2
3
java复制代码public
class Properties extends Hashtable<Object,Object> {
}

3. Properties对象使用

因为Properties是继承自Hashtable,而Hashtable是线程安全的Map容器,因此Properties也是线程安全的,同样的,在多线程并发获取配置的时候,它的性能表现也就不咋地了,why?

首先看一下配置获取

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 获取配置属性
public String getProperty(String key) {
Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}

// 获取配置属性,如果不存在,则返回默认值
public String getProperty(String key, String defaultValue) {
String val = getProperty(key);
return (val == null) ? defaultValue : val;
}

上面两个方法的使用频率很高,从签名上也很容易知道使用姿势;接下来需要看一下的为啥说并发效率很低

关键点就在第一个方法的super.get(),它对应的源码正是

1
2
3
java复制代码public synchronized V get(Object key) {
// ...
}

方法签名上有synchronized,所以为啥说并发环境下的性能表现不会特别好也就知道原因了

除了获取配置之外,另外一个常用的就是更新配置

1
2
3
java复制代码public synchronized Object setProperty(String key, String value) {
return put(key, value);
}

4. 小结

本文介绍的知识点主要是properties配置文件的处理,使用同名的java类来操作;需要重点注意的是Properties类属于Hashtable的子类,同样属于容器的范畴

最后提一个扩展的问题,在SpringBoot的配置自动装载中,可以将配置内容自动装载到配置类中,简单来讲就是支持配置到java bean的映射,如果现在让我们来实现这个,可以怎么整?

系列博文:

  • 实战小技巧1:字符串占位替换-JDK版
  • 实战小技巧2:数组与list互转
  • 实战小技巧3:字符串与容器互转
  • 实战小技巧4:优雅的实现字符串拼接
  • 实战小技巧5:驼峰与下划线互转
  • 实战小技巧6:枚举的特殊用法
  • 实战小技巧7:排序比较需慎重
  • 实战小技巧8:容器的初始化大小指定
  • 实战小技巧9:List.subList使用不当StackOverflowError
  • 实战小技巧10:不可变容器
  • 实战小技巧11:数组拷贝
  • 实战小技巧12:数字格式化

II. 其他

1. 一灰灰Blog: liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840
  • 微信公众号: 一灰灰blog

本文转载自: 掘金

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

SpringCloudAlibaba全网最全讲解4️⃣之Ri

发表于 2021-08-27

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

🌈往期回顾

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩**
  • SpringCloudAlibaba全网最全讲解3️⃣之Nacos(建议收藏)
  • SpringCloudAlibaba全网最全讲解2️⃣(建议收藏)
  • SpringCloudAlibaba全网最全讲解1️⃣(建议收藏)

六、负载均衡:Ribbon

6.1、负载均衡简介

负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。


根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡:
  1. 服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。
  2. 客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请
    求。

image-20210506110723494

我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供

者执行。

6.2、手动实现负载均衡

6.2.1、起一个不同端口的服务

我们需要借助IDEA再启一个服务,这个服务也是ShopProductServer,但是他的端口是不同的。


在VM option中输入需要指定的端口号即可:
1
markdown复制代码-Dserver.port=8081

image-20210505192515637

image-20210505192620065

6.2.2、修改实现类代码实现负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码 @Override
public Order getById(Long oid, Long pid) {
// 从nacos中获取服务
List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
// 定义一个随机数
int index = new Random().nextInt(instances.size());
// 随机获取一个服务
ServiceInstance serviceInstance = instances.get(index);
String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
Product product = restTemplate
.getForObject("http://"+url+"/product?productId=" + pid, Product.class);
Order order = orderDao.getOne(oid);
order.setUsername(order.getUsername()+",data form "+serviceInstance.getPort());
return order;
}

连续测试多几次就可以发现随机的负载均衡策略实现了。

image-20210505195610892

image-20210505195620979

6.3、基于Ribbon实现负载均衡

6.3.1、Ribbon是什么

Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。\*\*Ribbon就是简化了上面代码的组件,其中提供了更多的负载均衡算法。\*\*它是Spring Cloud的一个组件,它可以让我们使用一个注解就能轻松的搞定负载均衡。

6.3.2、Ribbon实现负载均衡

6.3.2.1、添加注解

**我们需要在RestTemplate的生成方法上添加一个注解:`@LoadBalance`,这个注解是一个增强注解,它可以增强RestTemplate ,使他可以实现负载均衡。**
1
2
3
4
5
java复制代码  @Bean
@LoadBalanced
public RestTemplate getInstance(){
return new RestTemplate();
}

6.3.2.2、修改ProductService实现类的代码

为了更直观,我们直接1将application.yml中的端口号设置到实现类中。
1
2
3
4
5
6
7
8
9
java复制代码  @Value("${server.port}")
private String port;

@Override
public Product findById(Long productId) {
Product product = productDao.findById(productId).get();
product.setPname(product.getPname()+",data form"+port);
return product;
}

image-20210505201056244

image-20210505201108043

6.3.2.3、修改OrderService实现类的代码

1
2
3
4
5
6
7
8
java复制代码  @Override
public Order getById(Long oid, Long pid) {
Product product = restTemplate
.getForObject("http://product-service/product?productId=" + pid, Product.class);
Order order = orderDao.getOne(oid);
order.setPname(product.getPname());
return order;
}

6.3.2.4、实现原理

为什么使用服务的名称替代url就可以实现负载均衡了呢?
  1. 通过反射获取到你传递过来的参数(假设传进来1),此时的url变成了:http://product-service/product?productId=1.
  2. 按照规则进行切割,将product-service切割出来。
  3. 根据从注册中心拉去过来的服务列表对应着的节点信息,假设1product-service对应着192.168.10.180:8081和192.168.10.180:8083。
  4. 根据你1配置的负载均衡的策略去选择一个信息节点,将服务名称替换成对应的ip和端口。假设此时url变成了:

http://192.168.10.180:8081/product?productId=1。

  1. 使用RestTemplate去发送请求。

注意:一定要在RestTemplate的生成方法上添加@LoadBalance注解,这个注解表示将RestTemplate进行加强,只有进行了加强才会去实现上述的步骤,不然是无法实现负载均衡的。

6.4、Ribbon支持的负载均衡策略

Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为`com.netflix.loadbalancer.IRule`,他具体支持的负载均衡策略有:
策略名 策略描述 实现说明
BestAvailableRule 选择一个最小的并发请求的server 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server
AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例; 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态
WeightedResponseTimeRule 根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低。 一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权
RetryRule 对选定的负载均衡策略机上重试机制。 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
RoundRobinRule 轮询方式轮询选择server 轮询index,选择index对应位置的server
RandomRule 随机选择一个server 在index上随机,选择index对应位置的server
ZoneAvoidanceRule(默认) 复合判断server所在区域的性能和server的可用性选择server 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。

我们可以通过修改配置来调整Ribbon的负载均衡策略,在Shop-order-server项目的application.yml中增加如下配置:

1
2
3
yaml复制代码product-service: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

本文转载自: 掘金

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

1…547548549…956

开发者博客

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