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

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


  • 首页

  • 归档

  • 搜索

【ExcelUtil】二次封装,注解驱动,用起来不要太舒服!

发表于 2021-11-23

前言

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战 。话不多说,要说就是为了补上篇留下的坑 【ExcelUtil】实现文件写出到客户端下载全过程 - 掘金 (juejin.cn)。

需求分析

除了最基础的表头名转换、表头和内容列宽自适应居中外,还需增加对表头顺序位置的指定,指定导出的日期数据时间日期格式,马达马达,对于枚举内容希望能够通过指定的分隔符读取写入值,此外,对于无数据的单元格可以按照需求给默认值 ……

image.png

最后,给一个是否导出数据标识用来应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写。

image.png

我:

熊猫人.gif

代码实现

自定义注解

首先,根据需求自定义一个注解,其中的每个属性对应一个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码
/**
* @description: 自定义导出 Excel 数据注解
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:37
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {

/**
* 导出到 Excel 中的表头别名
*/
String headerAlias() default "";

/**
* 导出时在 Excel 中的排序
*/
int sort() default Integer.MAX_VALUE;

/**
* 日期格式,如: yyyy-MM-dd
*/
String dateFormat() default "";

/**
* 根据分隔符读取内容转表达式 (如: 0=男,1=女,2=未知)
*/
String readConverterExp() default "";

/**
* 分隔符(默认为 "," 逗号),读取字符串组内容(注意:有些特殊分割字符需要用 "\\sparator" 或 "[sparator]"进行转义,否则分割字符串失败)
*/
String separator() default ",";

/**
* 当值为空时,字段的默认值
*/
String defaultValue() default "";

/**
* 是否导出数据
*/
boolean isExport() default true;

enum Type {
/** 导出导入 */
ALL(0),
/** 仅导出 */
EXPORT(1),
/** 仅导入 */
IMPORT(2);

private final int value;

Type(int value) {
this.value = value;
}

public int value() {
return this.value;
}
}

/**
* 字段类型(0:导出导入;1:仅导出;2:仅导入)
*/
Type type() default Type.ALL;
}

注解中有一个 Type 内部枚举类,用来区分被注解标识字段是导入还是导出,虽然这里的需求只要做导出,防范于未然,帮助你立身于需求高地。

image.png

工具类封装

通过 new ExcelUtil<>(xxx.class); 来创建二次封装对象,ExcelUtil<T> 类中
包含文件名、工作表名等基本属性,还有注解字段列表用来存储通过反射获取被注解标识的 Field 字段对象和对应的注解属性,内部存储结构为:[[Field, Excel], …] 。

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
java复制代码
/**
* @description: ExcelUtil 工具类二次封装
* @author: HUALEI
* @date: 2021-11-20
* @time: 17:56
*/
public class ExcelUtil<T> {

private static final Logger logger = LoggerFactory.getLogger(ExcelUtil.class);

/**
* Excel 文件名
*/
private String fileName;

/**
* 工作表名称
*/
private String sheetName;

/**
* 导出类型
*/
private Excel.Type type;

/**
* 文件名后缀
*/
private String fileNameSuffix;

/**
* 导入导出数据源列表
*/
private List<T> sourceList;

/**
* 注解字段列表 [[Field, Excel], ...]
*/
private List<Object[]> fields;

/**
* 实体对象
*/
public Class<T> clazz;

/**
* Excel 写出器
*/
public ExcelWriter excelWriter;

public ExcelUtil(Class<T> clazz) {
this.clazz = clazz;
}

......
......
}

封装类中除了成员变量外,最重要的就是成员方法了,考虑到导出的文件可能有时会需要 .xls 格式,所以我重载了导出 Excel 方法,默认为 .xlsx 格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码
/**
* 对数据源列表写入到 Excel 文件中
*
* @param response HttpServletResponse 对象
* @param list 数据源列表
* @param fileName Excel 文件名
* @param sheetName Excel 中工作表名
*/
public void exportExcel(HttpServletResponse response,
List<T> list,
String fileName,
String sheetName
) throws Exception {
this.excelWriter = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
logger.info("=============== 初始化 Excel ===============");
init(list, fileName, sheetName, Excel.Type.EXPORT);
exportExcel(response, null);
logger.info("=============== 导出 Excel 成功 ===============");
}

/**
* 对数据源列表写入到 Excel 文件中
*
* @param response HttpServletResponse 对象
* @param list 数据源列表
* @param fileName Excel 文件名
* @param fileNameSuffix Excel 文件名后缀
* @param sheetName Excel 中工作表名
*/
public void exportExcel(HttpServletResponse response,
List<T> list,
String fileName,
String fileNameSuffix,
String sheetName
) throws Exception {
this.excelWriter = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
logger.info("=============== 初始化 Excel ===============");
init(list, fileName, sheetName, Excel.Type.EXPORT);
exportExcel(response, fileNameSuffix);
logger.info("=============== 导出 Excel 成功 ===============");
}

导出方法中,首先就是要初始化写入器,然后初始化类属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码
/**
* 初始化类属性
*
* @param list 数据源列表
* @param fileName 导出文件名
* @param sheetName 工作表名
* @param type 导出类型
*/
public void init(List<T> list, String fileName, String sheetName, Excel.Type type) throws Exception {
this.sourceList = Optional.ofNullable(list).orElseGet(ArrayList<T>::new);
this.fileName = fileName;
this.sheetName = sheetName;
// 设置 Sheet 工作表名称
this.excelWriter.renameSheet(sheetName);
this.type = type;
// 创建表头
createExcelField();
// 处理数据源
handleDataSource();
}

初始化部分成员变量后,创建指定顺序表头,并设置表头别名:

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
java复制代码
/**
* 创建指定顺序表头,并设置表头别名
*/
private void createExcelField() {
this.fields = new ArrayList<Object[]>();

// 临时存储变量
List<Field> tempFields = new ArrayList<>();

// 获取目标实体对象所有声明字段列表,放入临时存储变量当中
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));

// 在声明的字段列表中过滤出被 @Excel 标记的字段
tempFields.stream()
.filter(field -> field.isAnnotationPresent(Excel.class))
.forEach(field -> {
// 获取注解属性对象
Excel attr = field.getAnnotation(Excel.class);
// 筛选目标导出类型
if (attr != null && (attr.type() == Excel.Type.ALL || attr.type() == this.type)) {
// 填充注解列表 [[Field, Excel]]
this.fields.add(new Object[]{ field, attr });
}
});

// 根据注解中 sort 属性值进行升序排序
this.fields.stream()
.sorted(Comparator.comparing( arr -> ((Excel) arr[1]).sort() ))
.collect(Collectors.toList())
// 按顺序设置表头别名
.forEach(arr -> {
String fieldName = ((Field) arr[0]).getName();
Excel attr = (Excel) arr[1];
this.excelWriter.addHeaderAlias(fieldName, StrUtil.isBlank(attr.headerAlias()) ? fieldName : attr.headerAlias());
});
}

先获取目标实体对象的父类和自身所有声明字段,存入临时字段列表,然后循环遍历过滤出被 @Excel 注解标识的字段,然后通过筛选目标导出类型构建一个大小为 2 的数组放入注解字段列表 this.fields 中。

其次,根据注解中 sort 属性值进行升序排序,如果全未设置顺序值,则默认根据字段定义的先后顺序进行排序。排序好之后按顺序设置表头别名,未设置的保持默认字段名。

创建完表头后,接下来就需要根据注解字段列表 fields 中每个字段上的注解属性对象对数据源列表进行处理:

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复制代码
/**
* 根据注解属性处理数据源列表
*
* @throws Exception 获取类属性值可能抛出的异常
*/
private void handleDataSource() throws Exception {
for (Object[] arr : this.fields) {
// 注解标识的字段
Field field = (Field) arr[0];
// 注解属性对象
Excel attr = (Excel) arr[1];
// 设置实体类私有属性可访问
field.setAccessible(true);

for (T object: this.sourceList) {
// 获取当前字段的属性值
Object value = field.get(object);
if (attr.isExport()) {
if (value != null) {
// 设置时间格式
if (StrUtil.isNotBlank(attr.dateFormat())) {
field.set(object, cn.hutool.core.convert.Convert.convert(field.getType(), DateUtil.format(new DateTime(value.toString()), attr.dateFormat())));
}
// 设置转换值
if (StrUtil.isNotBlank(attr.readConverterExp())) {
String convertResult = convertByExp(Convert.toStr(value), attr.readConverterExp(), attr.separator());
field.set(object, convertResult);
}
} else {
// 设置默认值
if (StrUtil.isNotBlank(attr.defaultValue())) {
field.set(object, attr.defaultValue());
}
}
} else {
field.set(object, null);
}
}
}
}

上述代码主要通过 Java 反射原理拿到当前对象 object 下 field 字段的属性值,判断当前列数据是否需要导出,需要则进一步判断注解中的属性对应的是否有值,有值且字段属性值不为 null,就去更改原有值;有值但字段属性值为 null 的,就可以设置为指定的默认值。反之,不需要导出,则将该列所有单元格置空。

单纯理解文字可能没有一个流程图来得直观、清楚,这就给你安排上:

handleDataSource() 方法执行流程图.png

对于解析导出值方法 convertByExp(),通过分隔符分割翻译注解字符串,根据 “=“ 等于号左边为键、右边为值原则进行解析,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码
/**
* 解析导出值
*
* @param propertyValue 参数值
* @param converterExp 翻译注解
* @param separator 分隔符
* @return 解析后值
*/
public static String convertByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
String[] convertSource = converterExp.split(separator);
for (String item : convertSource) {
String[] itemArray = item.split("=");
if (StringUtils.containsAny(separator, propertyValue)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[0].equals(value)) {
propertyString.append(itemArray[1]).append(separator);
break;
}
}
}
else {
if (itemArray[0].equals(propertyValue)) {
return itemArray[1];
}
}
}
return StringUtils.stripEnd(propertyString.toString(), separator);
}

以上就完成所有的初始化的工作了,接下来就可以愉快地往 Excel 里写数据,最后写出文件到客户端进行下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
/**
* 写出到客户端下载
*
* @param response HttpServletResponse 对象
* @param suffix 导出 Excel 文件名后缀
*/
public void exportExcel(HttpServletResponse response, String suffix) throws IOException {
// 输出流
ServletOutputStream out = response.getOutputStream();

this.excelWriter.write(this.sourceList, true);
cellWidthSelfAdaption();

initResponse(response, suffix);

this.excelWriter.flush(out, true);
// 关闭 writer,释放内存
this.excelWriter.close();
// 关闭输出 Servlet 流
IoUtil.close(out);
}
  • cellWidthSelfAdaption() 方法是用来实现中文宽度自适应的,这里就不贴代码了,详细说明和代码获取请点这里 传送门 (づ ̄3 ̄)づ╭❤~
  • initResponse() 根据导出的 Excel 文件名后缀初始化 HttpServletResponse 对象来响应体和响应类型。
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
java复制代码
/**
* 根据导出的 Excel 文件名后缀初始化 HttpServletResponse 对象
*
* @param response HttpServletResponse 对象
* @param suffix 文件名后缀
* @throws UnsupportedEncodingException 不支持的编码异常
*/
public void initResponse(HttpServletResponse response, String suffix) throws UnsupportedEncodingException {
// 默认导出文件名后缀
this.fileNameSuffix = ".xlsx";
if (suffix != null) {
switch (suffix.toLowerCase()) {
case "xls":
case ".xls":
this.fileNameSuffix = ".xls";
response.setContentType("application/vnd.ms-excel;charset=utf-8");
break;
case "xlsx":
case ".xlsx":
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
break;
default:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
}
} else {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
}
// 文件名中文编码
String encodingFilename = encodingFilename(this.fileName);
response.setHeader("Content-Disposition","attachment;filename="+ encodingFilename);
}

默认导出文件格式为 .xlsx ,不过也可指定为 .xls,通过设置不同的内容类型实现。至于导出的文件名加个后缀编个码拼接到响应头上即可!

1
2
3
4
5
6
7
8
9
10
java复制代码
/**
* 编码文件名
*
* @param filename 文件名
*/
public String encodingFilename(String filename) throws UnsupportedEncodingException {
filename = filename + this.fileNameSuffix;
return URLEncoder.encode(filename, CharsetUtil.UTF_8);
}

至此,整个注解 + ExcelUtil 二次封装的代码就写完了。

image.png

暴露接口

实体对象

老样子,实体对象给它套上 @Excel 注解,随便加点属性 “ Buff “:

063985D5.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
java复制代码
@Data
public class ProvinceCustomAnnotationExcelVO implements Serializable {

private static final long serialVersionUID = 877981781678377000L;

/**
* 省份
*/
@Excel(headerAlias = "省份")
private String province;

/**
* 省份的简称
*/
@Excel(headerAlias = "简称")
private String abbr;

/**
* 省份的面积(km²)
*/
@Excel(headerAlias = "面积(km²)")
private Integer area;

/**
* 省份的人口(万)
*/
@Excel(headerAlias = "人口(万)")
private BigDecimal population;

/**
* 省份的著名景点
*/
@Excel(headerAlias = "著名景点")
private String attraction;

/**
* 省会的邮政编码
*/
@Excel(headerAlias = "邮政编码", readConverterExp = "100=牛逼就完事|050000=哈哈哈", separator = "\\|")
private String postcode;

/**
* 省会名
*/
@Excel(headerAlias = "省会", defaultValue = "默认值")
private String city;

/**
* 省会的别名
*/
@Excel(headerAlias = "别名", isExport = false)
private String nickname;

/**
* 省会的气候类型
*/
@Excel(headerAlias = "气候类型")
private String climate;

/**
* 省会的车牌号
*/
@Excel(headerAlias = "车牌号", defaultValue = "数据暂无")
private String carcode;

/**
* 测试时间
*/
@Excel(headerAlias = "创建时间", dateFormat = "yyyy年MM月dd日 HH时mm分ss秒")
private String createTime;
}

控制层

Service 层 getAllProvinceDetails() 方法具体代码实现请参考 【ExcelUtil】实现文件写出到客户端下载全过程 - 掘金 (juejin.cn) 。

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复制代码
@GetMapping("provinces/custom/excel/export/{fileNameSuffix}")
public void customAnnotationExcelExport(HttpServletResponse response, @PathVariable("fileNameSuffix") String fileNameSuffix) throws Exception {
// 获取省份详情信息
List<ProvinceExcelVO> provinceExcelList = this.provinceService.getAllProvinceDetails();
// Bean 对象转换拿到数据源列表
List<ProvinceCustomAnnotationExcelVO> provinceCustomAnnotationExcelList = BeanUtil.copyToList(provinceExcelList, ProvinceCustomAnnotationExcelVO.class);

// 为了测试导出时间格式化,添加点随机日期时间
provinceCustomAnnotationExcelList.forEach(p -> p.setCreateTime(RandomUtil.randomDate(new Date(), DateField.SECOND, 0, 24*60*60).toString()));

// 使用有参构造(必需)创建一个 ExcelUtil 对象
ExcelUtil<ProvinceCustomAnnotationExcelVO> excelUtil = new ExcelUtil<>(ProvinceCustomAnnotationExcelVO.class);

// 文件名(当天日期_各省份信息)
String fileName = StrUtil.format("{}{}各省份信息", DateUtil.today(), StrUtil.UNDERLINE);
// Sheet 工作表名
String sheetName = "省份详情表";

if (StrUtil.isBlank(fileNameSuffix)) {
// 测试导出默认格式
excelUtil.exportExcel(response, provinceCustomAnnotationExcelList, fileName, sheetName);
} else {
// 测试导出指定格式
excelUtil.exportExcel(response, provinceCustomAnnotationExcelList, fileName, fileNameSuffix, sheetName);
}
}

导出的文件名后缀放在路径上主要是为了测试的方便,实际开发中 duck 不必这样!

接口测试

image.png

开始测试:

GET: http://localhost:8088/file/provinces/custom/excel/export/xls

image.png

GET: http://localhost:8088/file/provinces/custom/excel/export/.xlsx

image.png

GET: http://localhost:8088/file/provinces/custom/excel/export/""

image.png

GET: http://localhost:8088/file/provinces/custom/excel/export/HUALEI

image.png

测试全部通过,堪称完美,填坑成功!!撒花 ✿✿ヽ(°▽°)ノ✿

总结

总体实现下来并不算太难,使用注解驱动简直不要太香,用起来很方便,即便没学过编程的小白也会用,一两行代码就能完成一个数据源列表的导出。

image.png

唯一不足的就是数据导入没有集成进去,不过本文重点并不在于导入,哈哈哈,有兴趣的小伙伴可以尝试一下哦 ヾ(◍°∇°◍)ノ゙

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

本文转载自: 掘金

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

微信小程序加密数据解密算法-Java实现

发表于 2021-11-23

微信小程序加密介绍

我们开发微信小程序的过程中,我们的服务端有时需要获取微信提供的开放数据。微信会对这些开放数据做签名和加密处理。开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。

1637569797592-70223136-ae8c-4738-9468-80c58f0c0712.jpeg

官方详细介绍:developers.weixin.qq.com/miniprogram…

微信官方提供了多种编程语言的示例代码,但是目前下载的示例代码只有C++,Node,PHP,以及Python语言的,没有Java语言的,所以需要我们自己实现解密算法。

Bouncy Castle Crypto工具包解密

Bouncy Castle Crypto包是一个加密算法的Java实现。这个jar包含了用于JDK1.5及以上的JCE提供程序和Bouncy Castle加密的轻量级API。

引入依赖

Bouncy Castle Crypto包有许多版本的jar包,可以在Maven仓库(mvnrepository.com/)搜索bcprov-jdk,选择适合自己的版本jar,我这使用的是受欢迎比较多的bcprov-jdk15on,它适用于JDK1.5及以上版本。

gradle文件:

1
groovy复制代码 implementation "org.bouncycastle:bcprov-jdk15on:1.69"

maven文件:

1
2
3
4
5
xml复制代码 <dependency>
     <groupId>org.bouncycastle</groupId>
     <artifactId>bcprov-jdk15on</artifactId>
     <version>1.69</version>
 </dependency>

算法实现

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复制代码 /**
  * 解密微信加密数据,对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
  *
  * @param encryptedData 加密串
  * @param sessionKey   会话密钥
  * @param iv           解密算法初始向量
  * @return 解密后的数据
  */
 public static String decryptWxData(String encryptedData, String sessionKey, String iv) {
   try {
     // 初始化
     Security.addProvider(new BouncyCastleProvider());
     SecretKeySpec spec = new SecretKeySpec(Base64.decodeBase64(sessionKey), "AES");
     AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
     parameters.init(new IvParameterSpec(Base64.decodeBase64(iv)));
     Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
     cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
     byte[] resultByte = cipher.doFinal(Base64.decodeBase64(encryptedData));
     if (null != resultByte && resultByte.length > 0) {
       String result = new String(resultByte, StandardCharsets.UTF_8);
       log.info(">>>>> 微信加密数据解析结果:{}", result);
       return result;
    }
  } catch (Exception e) {
     log.error(">>>>> 微信加密数据解析失败:", e);
  }
   return null;
 }

Hutool工具类解密

Hutool是一个小而全的Java工具类库,封装了很多实用的静态方法,我们可以使用这个依赖包的相关工具类来简单封装,从而实现微信小程序加密数据的解密。

引入依赖

gradle文件:

1
groovy复制代码 implementation 'cn.hutool:hutool-all:5.7.16'

maven文件:

1
2
3
4
5
xml复制代码 <dependency>
     <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.7.16</version>
 </dependency>

算法实现

我们只需要两行代码即可实现数据解密,但是解密后的字节数组末尾可能带有补位字符,所以我们需要判断是否有补位字符,如果有则删除这些字符。

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复制代码 /**
  * 解密微信加密数据,对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
  *
  * @param encryptedData 加密串
  * @param sessionKey   会话密钥
  * @param iv           解密算法初始向量
  * @return 解密后的数据
  */
 public static String decryptWxDataOfHutool(String encryptedData, String sessionKey, String iv) {
   AES aes = new AES(Mode.CBC, Padding.NoPadding, Base64.decodeBase64(sessionKey),
       Base64.decodeBase64(iv));
   byte[] resultByte = aes.decrypt(Base64.decodeBase64(encryptedData));
   if (null != resultByte && resultByte.length > 0) {
     // 删除解密后明文的补位字符
     int padNum = resultByte[resultByte.length - 1];
     if (padNum < 1 || padNum > 32) {
       padNum = 0;
    }
     resultByte = Arrays.copyOfRange(resultByte, 0, resultByte.length - padNum);
     String result = new String(resultByte, StandardCharsets.UTF_8);
     log.info(">>>>> 微信加密数据解析结果:{}", result);
     return result;
  }
   return null;
 }

验证

这套加密算法是适用于微信小程序的绝大部分加密数据的,所以我们使用带有用户手机号的加密串,进行解密验证。

注意,验证测试的时候,需要将以下三个变量的值替换为自己小程序的,不然会报错。

1
2
3
4
5
6
7
8
9
java复制代码 public static void main(String[] args) {
   String encryptedData = "je8FUw5ivIMnbW/SUeJzEqb1dqiv8S1c1oJ+aVcktjnkdMcq16TLBbkiDug1Km2sxuu5nmO5j9QGLWREOAkaHwjJH/2lHliq8D3MSl9q9fE5WtNtwO0BsBh/DBhLVaRFwpwEWGhspRy9TL+PO8lezkb3E0xdZVdQKB0SbAqhDUe8k5j7fG4G6oxvvrg7DnxV8uKi67Mo1RGFAx1QjtffFw==";
   String sessionKey = "Eq5Bao4+Niiyqv\/GmEFR4A==";
   String iv = "zr75JDqgcc23AKQxmzZykA==";
 ​
   decryptWxDataOfHutool(encryptedData, sessionKey, iv);
 ​
   decryptWxData(encryptedData, sessionKey, iv);
 }

成功运行后,可以得到如下正确的数据:

1
2
3
4
5
6
7
8
9
json复制代码 {
     "phoneNumber":"18825412340",
     "purePhoneNumber":"18825412340",
     "countryCode":"86",
     "watermark":{
         "timestamp":1637287930,
         "appid":"wx17e20c3acb11ef2a"
    }
 }

完整代码

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
java复制代码 package com.chenpi;
 ​
 import cn.hutool.crypto.Mode;
 import cn.hutool.crypto.Padding;
 import cn.hutool.crypto.symmetric.AES;
 import java.nio.charset.StandardCharsets;
 import java.security.AlgorithmParameters;
 import java.security.Security;
 import java.util.Arrays;
 import javax.crypto.Cipher;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.binary.Base64;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 ​
 /*
  * @description 微信相关工具类
  * @author Mr.nobody
  * @date 2021/11/22
  * @version 1.0
  */
 @Slf4j
 public class WxUtils {
 ​
   /**
    * 解密微信加密数据,对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
    *
    * @param encryptedData 加密串
    * @param sessionKey   会话密钥
    * @param iv           解密算法初始向量
    * @return 解密后的数据
    */
   public static String decryptWxData(String encryptedData, String sessionKey, String iv) {
     try {
       // 初始化
       Security.addProvider(new BouncyCastleProvider());
       SecretKeySpec spec = new SecretKeySpec(Base64.decodeBase64(sessionKey), "AES");
       AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
       parameters.init(new IvParameterSpec(Base64.decodeBase64(iv)));
       Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
       cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
       byte[] resultByte = cipher.doFinal(Base64.decodeBase64(encryptedData));
       if (null != resultByte && resultByte.length > 0) {
         String result = new String(resultByte, StandardCharsets.UTF_8);
         log.info(">>>>> 微信加密数据解析结果:{}", result);
         return result;
      }
    } catch (Exception e) {
       log.error(">>>>> 微信加密数据解析失败:", e);
    }
     return null;
  }
 ​
   /**
    * 解密微信加密数据,对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
    *
    * @param encryptedData 加密串
    * @param sessionKey   会话密钥
    * @param iv           解密算法初始向量
    * @return 解密后的数据
    */
   public static String decryptWxDataOfHutool(String encryptedData, String sessionKey, String iv) {
     AES aes = new AES(Mode.CBC, Padding.NoPadding, Base64.decodeBase64(sessionKey),
         Base64.decodeBase64(iv));
     byte[] resultByte = aes.decrypt(Base64.decodeBase64(encryptedData));
     if (null != resultByte && resultByte.length > 0) {
       // 删除解密后明文的补位字符
       int padNum = resultByte[resultByte.length - 1];
       if (padNum < 1 || padNum > 32) {
         padNum = 0;
      }
       resultByte = Arrays.copyOfRange(resultByte, 0, resultByte.length - padNum);
       String result = new String(resultByte, StandardCharsets.UTF_8);
       log.info(">>>>> 微信加密数据解析结果:{}", result);
       return result;
    }
     return null;
  }
 ​
   public static void main(String[] args) {
     String encryptedData = "9d9sSqhjoZI9DgxC7f+wAnciEhgHv0WVitr6UAF8Wf7mS7PJniGsnaYE6I8oRnhpc10VDUuiHPm685NW0SbRVokaH9CDkVZnrezVQw8HR4RaFpgu/HNT1JchLKxRN4n+nqg7dlgGfwArPc8zBAMVrEXbE7QNuyu1oUBdX9wFuFAaBhijg2kPDwcJuwfrBLl+CB/D9O//3Z8e7O07Cdn5/TaETkpfm/Ep82efZIhOQ3h3iumF+YhkVcpi/anc1EMwEr5G2aJyA1XebGT7PsTT6j9gZJbTlOGUJGYM5Ny4SJSU3nRwLE0YZXwX7prR/JQGCg/VhwxWPMAPLgSIFJf2mbpcog4jzxARK8pQNrWXyCJfA+rUWrCzfXFatpj4Qw6xq9KomawpPWFvdOJdULHig8G3e9WaR/Rtq+S3hWRxf+U9MGvlx0hQf3gJVLpOFkqtL0gy9r3vIMbDbRjK1dbqF4rQP+Xu9HREJaU1s+EUY7DEnjPU1PC3F32T1fo9ZWnf/Hyk2+xGYAQjoUXJv/gzlrv05dYDOr+phvWVYJ4Bgw5WbzAc/v4hlI4fy4cPg2P2xjJzopeyXaT1HiZNbkFiTtun7DoE6neXs2zUnXaVpkANmGQqQI2AtYsBUKJ1GIgXgh4n5s2uw3lFTFQrq80lvoXmH+1r9oPaHOYwHwkzocOdgTBLMdiBzNVUVT49wswfUFZUMcAKkqUCRDWdNr8gVgkEdHDUmnt4SfLsrMBuB81UZLTKGfd6c9NzDNlwYy9JzAbpKlr62ppilYM4sgh1HQy0d25FGJQ+TcJDy+RHSdJMY+8S+EpLCylXedAsXtRzpTU/BwgkDYmeizM86vbVq1h9qgmU0Emv8JJtKQhS7CtyXrL880eQ/ze/eLSEs6RC8s0B016xd1vXQvOrovbyqlB+j+fWEyzoSrTlUwATdepkiXle21YDYSy6nf6fi7hYjfh/3WFLwfYqXp7xuP1ndp5TuuhU9ye485G+kpViMpWVo69MOvFIQhUGXewfmW3U6GrwK1dBaNxk6UNquqosOBQ6ufmt6u9Y6pHqiRxKKA3N/aMrOd1A81p87JOmX3bCp3tgU8k5w+de3Ual4hdDcHK8AJUfrT8H5BDtY5LP2Ny/rD3X6VuO9//WQpszEFprosDZcb0SBTG3dxeIr4lb1I9MHVtJoEOfwLV8WtUN6Rdn/kGZ1nInSAuMyqZLBRfAX2603lv91SkghnY+FT3PCuqaDPDEDUgd/ozM9DWfaI9eaC/OC5NU+WqW9s17Q3+cLaWF7VZswloRuS/8LMrahxmTcDnIFRV+S1SeyV+Bdm79qseeQEdsSVDdLO2aJ1iViLxzbebb8pUIhx10JMacUrOBFVB+J76lLR4AiVT8rydLRA6uq5fFCMlg2GDXVBkbgqUoBVuiozGLKjPPSmfbk5s9JMxa8zSTZ6Vq4EefAeWoSKiWKHtBkFWcUEbp6I2+YycS3kqP8kC069csssGcHqDA6ouP+wEkpoDVS3vFi4fVkpjBtpY5Fu+NIQYUfiC54EA2JMyBS3lHGv01oPW54o4uSZoglcb2S6Yf9fkgS48ft0xE7Yi0YTQBct2jVTOSq5jjwxb26pjAhfkkccdvjZD3wHZSl/dNEsOWdKrdhE6H93Rvqny/nvQFX0DDzZPs1O2rbRAGxgHtKSDfxjWC6g==";
     String sessionKey = "VrgQ9xkONam93c+ULD0yLA==";
     String iv = "W4ND0KBskxNNnSRCuapaqQ==";
 ​
     decryptWxDataOfHutool(encryptedData, sessionKey, iv);
 ​
     decryptWxData(encryptedData, sessionKey, iv);
  }
 }

本次分享到此结束啦~~

如果觉得文章对你有帮助,点赞、收藏、关注、评论,您的支持就是我创作最大的动力!

本文转载自: 掘金

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

网不好怎么办?TLS握手带宽直降80%,BabaSSL是怎么

发表于 2021-11-23

简介:为了保障数据的安全性,客户端会先和服务器进行 TLS 握手,有什么办法可以减少 TLS 握手的带宽消耗呢?

编者按:BabaSSL 是一款开源的密码库产品,在 GitHub 和龙蜥社区开源,并加入到龙蜥社区(OpenAnolis)的商密软件栈 SIG 组,且作为 Anolis 商密 OS 的核心组件之一。本文作者张成龙(刻一),是蚂蚁集团技术专家,负责BabaSSL密码库产品,也是OpenSSL贡献者之一。

前言

随着 5G 网络的建设,加速了移动互联网应用的发展,包括短视频、在线教育、物联网等领域。但是在现实生活中,依然存在网络信号不好的场景,包括地下商场、车库、地铁等地方,或者是由于网络拥塞导致的弱网环境下,应用在使用过程中加载缓慢,导致用户体验变差。这时候就需要对弱网环境进行优化,而手段之一就是想办法降低网络数据传输。

为了保障数据的安全性,通常使用 TLS/SSL 进行加密传输。当客户端访问服务器后台时,客户端会先和服务器进行 TLS 握手。在 TLS 完整握手时,服务端会发送证书链用于身份认证,而握手时数据传输的大部分都来自于证书。

有什么办法可以减少 TLS 握手的带宽消耗呢?如果证书可以被压缩,甚至“消失”,那就可以大大降低数据传输。RFC 8879 TLS Certificate Compression 就是为了解决这个问题,在 TLS 1.3 握手时提供证书压缩功能。

BabaSSL 是一款开源的密码库产品,在 GitHub 和 OpenAnolis 龙蜥社区开源,并加入到龙蜥社区(OpenAnolis)的商密软件栈 SIG 组,且作为 Anolis 商密 OS 的核心组件之一,替代 OpenSSL 成为系统默认的基础密码算法库;基于 OpenSSL 1.1 版本并保持兼容性,在 OpenSSL 的基础上实现了一系列自主可控的安全特性,包括各种国密算法、国密标准的实现以及大量提高密钥安全强度的特性实现。目前,BabaSSL 已经支持 TLS 证书压缩功能,而 OpenSSL 还不支持。

TLS 证书压缩介绍

1、如果客户端支持证书压缩,在 ClientHello 消息中携带 compress_certificate 扩展,该扩展中包含支持的压缩算法列表;

2、服务端收到 ClientHello,发现对方支持证书压缩,如果服务端也支持证书压缩,同时支持客户端声明的压缩方法,则使用该算法压缩 Certificate 消息;

3、服务端发送 CompressedCertificate 消息,代替原来的 Certificate 消息,CompressedCertificate 消息中包含压缩算法,解压后的长度和压缩的 Certificate 消息;

4、 客户端收到 CompressedCertificate 消息后,使用其中的 algorithm 解压,如果解压成功,则进行后续处理,否则关闭连接并发送 bad_certificate 警告。服务端发送 CertificateRequest 消息,然后客户端发送 CompressedCertificate 消息的处理流程和上述类似,不再赘述。

标准中定义的压缩算法:

除了 RFC 中定义的这 3 种算法,用户还可以使用其他算法,值 16384 到 65535 用于留给用户自已使用。

实战TLS证书压缩

开源 BabaSSL 密码库已经支持 TLS 证书压缩功能,需要在构建 BabaSSL 时开启该功能,config 后添加 enable-cert-compression。

可以在设置 SSL_CTX 时,添加证书压缩算法,代码示例如下:

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
arduino复制代码#include <openssl/ssl.h>
#include <zlib.h>

static int zlib_compress(SSL *s,
const unsigned char *in, size_t inlen,
unsigned char *out, size_t *outlen)
{

if (out == NULL) {
*outlen = compressBound(inlen);
return 1;
}

if (compress2(out, outlen, in, inlen, Z_DEFAULT_COMPRESSION) != Z_OK)
return 0;

return 1;
}

static int zlib_decompress(SSL *s,
const unsigned char *in, size_t inlen,
unsigned char *out, size_t outlen)
{
size_t len = outlen;

if (uncompress(out, &len, in, inlen) != Z_OK)
return 0;

if (len != outlen)
return 0;

return 1;
}

int main() {
const SSL_METHOD *meth = TLS_client_method();
SSL_CTX *ctx = SSL_CTX_new(meth);

/* 配置证书、私钥... */

/* 例如:设置压缩算法为zlib */
SSL_CTX_add_cert_compression_alg(ctx, TLSEXT_cert_compression_zlib,
zlib_compress, zlib_decompress);

SSL *con = SSL_new(ctx);

/* 握手... */

return 0;
}

也可以使用 BabaSSL 提供的 s_client 和 s_server 来使用 TLS 证书压缩功能:

1
2
3
4
5
bash复制代码# 服务端
/opt/babassl/bin/openssl s_server -accept 127.0.0.1:34567 -cert server.crt -key server.key -tls1_3 -cert_comp zlib -www -quiet

# 客户端
/opt/babassl/bin/openssl s_client -connect 127.0.0.1:34567 -tls1_3 -cert_comp zlib -ign_eof -trace

测试压缩算法和压缩率

服务端配置证书链,CA 证书 + 中间 CA + 域名证书,TLS 1.3 握手且开启证书压缩,对比各个压缩算法的压缩率如下:

有些压缩算法是支持设置字典的,比如 brotli、zstd。可以提前计算好字典内容,预埋到客户端和服务端,然后在压缩和解压的时候使用该字典,可以让证书链完美“消失”。例如上表中使用 zstd + 字典时,压缩前的 Certficate 消息为 2666 字节,压缩后只有 18 字节。

开启证书压缩功能后,可以大大降低握手时的传输,尤其是使用字典时,例如 zstd + 字典时数据如下:

关闭证书压缩,握手共传输:3331 字节

开启证书压缩:握手共传输:698 字节

压缩率:698/3331 * 100% = 20.95%,握手带宽降低接近 80%。

结语

TLS 会话复用时不需要发送证书,所以,在完整握手时,就可以通过证书压缩来优化。在双向认证的场景下,即服务端开启了客户端认证,如果客户端和服务端都开启 TLS 证书压缩功能,压缩效果更明显,可以节省 TLS 握手中 80% 以上的带宽。后面 BabaSSL 还会支持 Compact TLS 1.3,即 TLS 1.3 的袖珍版,保持协议同构的前提下,占用最小的带宽。

原文链接

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

本文转载自: 掘金

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

什么是微服务架构,有何优缺点?

发表于 2021-11-23

什么是微服务架构?

通常而言,微服务架构是一种架构模式或者说是一种架构风格。它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。

微服务架构和单体架构的区别

单体架构

通俗地讲,“单体应用(monolith application)”就是将应用程序的所有功能都打包成一个独立的单元,可以是JAR、EXE、BIN或其它归档格式。

单体应用有如下优点:

开发简单直接,集中式管理, 基本不会重复开发;

功能都在本地,没有分布式的管理开销和调用开销;

它的缺点也非常明显,特别对于互联网公司来说:

开发效率低—所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断;

代码维护难—代码功能耦合在一起,新人不知道何从下手;

部署不灵活—构建时间长,任何小修改必须重新构建整个项目,这个过程往往很长;

稳定性不高—一个微不足道的小问题,可以导致整个应用挂掉;

扩展性不够—无法满足高并发情况下的业务需求。

微服务架构

随着业务需求的快速发展变化,敏捷性、灵活性和可扩展性需求不断增长,迫切需要一种更加快速高效的软件交付方式。微服务就是一种可以满足这种需求的软件架构风格。单体应用被分解成多个更小的服务,每个服务有自己的归档文件,单独部署,然后共同组成一个应用程序。这里的“微”不是针对代码行数而言,而是说服务的范围限定到单个功能。

微服务有如下优点:

微服务是松藕合的,无论是在开发阶段或部署阶段都是独立的。

能够快速响应, 局部修改容易, 一个服务出现问题不会影响整个应用。

易于和第三方应用系统集成, 支持使用不同的语言开发, 允许你利用融合最新技术。

每个微服务都很小,足够内聚,足够小,代码容易理解。团队能够更关注自己的工作成果, 聚焦指定的业务功能或业务需求。

开发简单、开发效率提高,一个服务可能就是专一的只干一件事, 能够被小团队单独开发,这个小团队可以是 2 到 5 人的开发人员组成。

同样的, 也存在如下缺点:

微服务架构带来过多的运维操作,可能需要团队具备一定的DevOps技巧。

分布式系统可能复杂难以管理。因为分布部署跟踪问题难,当服务数量增加,管理复杂性增加。

微服务架构的主要特点

微服务架构是一种松耦合的、有一定的有界上下文的面向服务架构。

也就是说,如果遇到一个功能变更, 但其要求每个服务都要同时修改,那么它们就不能称之为微服务,因为它们紧耦合在一起;如果你需要掌握一个服务的上下文场景使用条件,那么它就是一个有上下文边界的服务,这个定义一般来自DDD(领域驱动设计)。

它的主要特点是组件化、松耦合、自治、去中心化,体现在以下几个方面:

细粒度的服务分解,服务粒度要小,而每个服务是针对一个单一职责的业务能力的封装,专注做好一件事情。

独立部署运行和扩展,每个服务能够独立被部署并运行在一个进程内。这种运行和部署方式能够赋予系统灵活的代码组织方式和发布节奏,使得快速交付和应对变化成为可能。

独立开发和演化,技术选型灵活,不受遗留系统技术约束。合适的业务问题选择合适的技术可以独立演化。服务与服务之间采取与语言无关的API进行集成。相对单体架构,微服务架构是更面向业务创新的一种架构模式。

独立团队和自治,团队对服务的整个生命周期负责,工作在独立的上下文中,自己决策自己治理,而不需要统一的指挥中心。团队和团队之间通过松散的社区部落进行衔接。

通过解耦我们所做的事情,分而治之以减少不必要的损耗,使得整个复杂的系统和组织能够快速的应对变化。

需要考虑的问题

单个微服务代码量小,易修改和维护。但是,系统复杂度的总量是不变的,每个服务代码少了,但服务的个数肯定就多了。就跟拼图游戏一样,切的越碎,越难拼出整幅图。

一个系统被拆分成零碎的微服务,最后要集成为一个完整的系统,其复杂度肯定比大块的功能集成要高很多。单个微服务数据独立,可独立部署和运行。虽然微服务本身是可以独立部署和运行的,但仍然避免不了业务上的你来我往,这就涉及到要对外通信,当微服务的数量达到一定量级的时候,如何提供一个高效的集群通信机制成为一个问题。

单个微服务拥有自己的进程,进程本身就可以动态的启停,为无缝升级打好了基础,但谁来启动和停止进程,什么时机,选择在哪台设备上做这件事情才是无缝升级的关键。这个能力并不是微服务本身提供的,而是需要背后强大的版本管理和部署能力。

多个相同的微服务可以做负载均衡,提高性能和可靠性。正是因为相同微服务可以有多个不同实例,让服务按需动态伸缩成为可能,在高峰期可以启动更多的相同的微服务实例为更多用户服务,以此提高响应速度。同时这种机制也提供了高可靠性,在某个微服务故障后,其他相同的微服务可以接替其工作,对外表现为某个设备故障后业务不中断。

同样的道理,微服务本身是不会去关心系统负载的,那么什么时候应该启动更多的微服务,多个微服务的流量应该如何调度和分发,这背后也有一套复杂的负载监控和均衡的系统在起作用。微服务可以独立部署和对外提供服务,微服务的业务上线和下线是动态的,当一个新的微服务上线时,用户是如何访问到这种新的服务?这就需要有一个统一的入口,新的服务可以动态的注册到这个入口上,用户每次访问时可以从这个入口拿到系统所有服务的访问地址。这个统一的系统入口并不是微服务本身的一部分,所以这种能力需要系统单独提供。还有一些企业级关注的系统问题,比如,安全策略如何集中管理?系统故障如何快速审计和跟踪到具体服务?整个系统状态如何监控?服务之间的依赖关系如何管理?等等这些问题都不是单个微服务考虑的范畴,而需要有一个系统性的考虑和设计,让每个微服务都能够按照系统性的要求和约束提供对应的安全性,可靠性,可维护性的能力。

选择微服务框架的关注点

服务注册、发现、负载均衡和健康检查,假定采用进程内LB方案,那么服务自注册一般统一做在服务器端框架中,健康检查逻辑由具体业务服务定制,框架层提供调用健康检查逻辑的机制,服务发现和负载均衡则集成在服务客户端框架中。

监控日志,框架一方面要记录重要的框架层日志、Metrics和调用链数据,还要将日志、Metrics等接口暴露出来,让业务层能根据需要记录业务日志数据。在运行环境中,所有日志数据一般集中落地到企业后台日志系统,做进一步分析和处理。REST/RPC和序列化,框架层要支持将业务逻辑以HTTP/REST或者RPC方式暴露出来,HTTP/REST是当前主流API暴露方式,在性能要求高的场合则可采用Binary/RPC方式。针对当前多样化的设备类型(浏览器、普通PC、无线设备等),框架层要支持可定制的序列化机制,例如,对浏览器,框架支持输出Ajax友好的JSON消息格式,而对内部服务及应用程序,框架支持输出性能高的Binary消息格式。

配置,除了支持普通配置文件方式的配置,框架层还可集成动态运行时配置,能够在运行时针对不同环境动态调整服务的参数和配置。限流和容错,框架集成限流容错组件,能够在运行时自动限流和容错,保护服务,如果进一步和动态配置相结合,还可以实现动态限流和熔断。管理接口,框架集成管理接口,一方面可以在线查看框架和服务内部状态,同时还可以动态调整内部状态,对调试、监控和管理能提供快速反馈。Spring Boot微框架的Actuator模块就是一个强大的管理接口。统一错误处理,对于框架层和服务的内部异常,如果框架层能够统一处理并记录日志,对服务监控和快速问题定位有很大帮助。安全,安全和访问控制逻辑可以在框架层统一进行封装,可做成插件形式,具体业务服务根据需要加载相关安全插件。文档自动生成,文档的书写和同步一直是一个痛点,框架层如果能支持文档的自动生成和同步,会给使用API的开发和测试人员带来极大便利。Swagger是一种流行Restful API的文档方案。

一个完整的微服务系统,它最少要包含以下功能:

日志和审计,主要是日志的汇总,分类和查询

监控和告警,主要是监控每个服务的状态,必要时产生告警

消息总线,轻量级的MQ或HTTP

注册发现

负载均衡

部署和升级

事件调度机制

以下功能不是最小集的一部分,但也应该在选择时进行考虑:

认证和鉴权

多语言支持,是否支持多种编程语言

统一服务构建和打包

统一服务测试

统一配置文件管理

服务依赖关系管理

问题跟踪调试框架

灰度发布

蓝绿部署

资源管理,如:底层的容器, 虚拟机, 物理机和网络管理

开发方式影响

随着持续交付概念推广以及容器概念的普及,微服务将这两种理念和技术结合起来,形成新的微服务+API+容器平台的开发模式,提出了容器化微服务的持续交付概念。

下图为传统单体应用的DevOps开发队伍方式:

这种整体型架构要求产品队伍横跨产品管理 Dev开发 QA DBA 以及系统运营管理,而微服务架构引入以后,如下图:

微服务促进了 DevOps 方式的重组,将一个大臃肿的整体产品开发队伍切分为根据不同微服务的划分的产品队伍,以及一个大的整体的平台队伍负责运营管理,两者之间通过 API 交互,做到了松耦合隔绝。

微服务的实施是有一定的先决条件:基础的运维能力(如监控、快速配置、快速部署)需提前构建,否则就会陷入较被动的局面。推荐采用CI/CI改进基础设施及运维的实践,通过自动化运维使得可以快速安全的响应和处理微服务对服务部署的要求,通过容器技术保证服务环境之间拥有更高的一致性,降低“在我的环境工作,而你的环境不工作”的可能,也是为后续的发布策略和运维提供更好的支撑。

想要更好的实施微服务,首先需要考虑构建团队DevOps能力,这是保证微服务架构在持续交付和应对复杂运维问题的动力之源。其次保持服务持续演进,使之能够快速、低成本地被拆分和合并,以快速响应业务的变化;同时要保持团队和架构对齐。微服务看似是技术层面的变革,但它对团队结构和组织文化有很强的要求和影响。识别和构建匹配架构的团队是解决问题的另一大支柱。

最后,打造持续改进的组织文化是实施微服务的关键基石。只有持续改进,持续学习和反馈,持续打造这样一个文化氛围和团队,微服务架构才能持续发展下去,保持新鲜的生命力,从而实现我们的初衷。

文.无敌码农

本文转载自: 掘金

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

RabbitMQ 管理页面该如何使用

发表于 2021-11-23

@[toc]
RabbitMQ 的 web 管理页面相信很多小伙伴都用过,随便点一下估计也都知道啥意思,不过本着精益求精的思想,松哥还是想和大家捋一捋这个管理页面的各个细节。

  1. 概览

首先,这个 Web 管理页面大概就像下图这样:

首先一共有六个选项卡:

  1. Overview:这里可以概览 RabbitMQ 的整体情况,如果是集群,也可以查看集群中各个节点的情况。包括 RabbitMQ 的端口映射信息等,都可以在这个选项卡中查看。
  2. Connections:这个选项卡中是连接上 RabbitMQ 的生产者和消费者的情况。
  3. Channels:这里展示的是“通道”信息,关于“通道”和“连接”的关系,松哥在后文再和大家详细介绍。
  4. Exchange:这里展示所有的交换机信息。
  5. Queue:这里展示所有的队列信息。
  6. Admin:这里展示所有的用户信息。

右上角是页面刷新的时间,默认是 5 秒刷新一次,展示的是所有的 Virtual host。

这是整个管理页面的一个大致情况,接下来我们来逐个介绍。

  1. Overview

Overview 中分了如下一些功能模块:

分别是:

Totals:

Totals 里面有 准备消费的消息数、待确认的消息数、消息总数以及消息的各种处理速率(发送速率、确认速率、写入硬盘速率等等)。

Nodes:

Nodes 其实就是支撑 RabbitMQ 运行的一些机器,相当于集群的节点。

点击每个节点,可以查看节点的详细信息。

Churn statistics:

这个不好翻译,里边展示的是 Connection、Channel 以及 Queue 的创建/关闭速率。

Ports and contexts:

这个里边展示了端口的映射信息以及 Web 的上下文信息。

  • 5672 是 RabbitMQ 通信端口。
  • 15672 是 Web 管理页面端口。
  • 25672 是集群通信端口。

Export definitions && Import definitions:

最后面这两个可以导入导出当前实例的一些配置信息:

  1. Connections

这里主要展示的是当前连接上 RabbitMQ 的信息,无论是消息生产者还是消息消费者,只要连接上来了这里都会显示出来。

注意协议中的 AMQP 0-9-1 指的是 AMQP 协议的版本号。

其他属性含义如下:

  • User name:当前连接使用的用户名。
  • State:当前连接的状态,running 表示运行中;idle 表示空闲。
  • SSL/TLS:表示是否使用 ssl 进行连接。
  • Channels:当前连接创建的通道总数。
  • From client:每秒发出的数据包。
  • To client:每秒收到的数据包。

点击连接名称可以查看每一个连接的详情。

在详情中可以查看每一个连接的通道数以及其他详细信息,也可以强制关闭一个连接。

  1. Channels

这个地方展示的是通道的信息:

那么什么是通道呢?

一个连接(IP)可以有多个通道,如上图,一共是两个连接,但是一共有 12 个通道。

一个连接可以有多个通道,这个多个通道通过多线程实现,一般情况下,我们在通道中创建队列、交换机等。

生产者的通道一般会立马关闭;消费者是一直监听的,通道几乎是会一直存在。

上面各项参数含义分别如下:

  • Channel:通道名称。
  • User name:该通道登录使用的用户名。
  • Model:通道确认模式,C 表示 confirm;T 表示事务。
  • State:通道当前的状态,running 表示运行中;idle 表示空闲。
  • Unconfirmed:待确认的消息总数。
  • Prefetch:Prefetch 表示每个消费者最大的能承受的未确认消息数目,简单来说就是用来指定一个消费者一次可以从 RabbitMQ 中获取多少条消息并缓存在消费者中,一旦消费者的缓冲区满了,RabbitMQ 将会停止投递新的消息到该消费者中直到它发出有消息被 ack 了。总的来说,消费者负责不断处理消息,不断 ack,然后只要 unAcked 数少于 prefetch * consumer 数目,RabbitMQ 就不断将消息投递过去。
  • Unacker:待 ack 的消息总数。
  • publish:消息生产者发送消息的速率。
  • confirm:消息生产者确认消息的速率。
  • unroutable (drop):表示未被接收,且已经删除了的消息。
  • deliver/get:消息消费者获取消息的速率。
  • ack:消息消费者 ack 消息的速率。
  1. Exchange

这个地方展示交换机信息:

这里会展示交换机的各种信息。

Type 表示交换机的类型。

Features 有两个取值 D 和 I。

D 表示交换机持久化,将交换机的属性在服务器内部保存,当 MQ 的服务器发生意外或关闭之后,重启 RabbitMQ 时不需要重新手动或执行代码去建立交换机,交换机会自动建立,相当于一直存在。

I 表示这个交换机不可以被消息生产者用来推送消息,仅用来进行交换机和交换机之间的绑定。

Message rate in 表示消息进入的速率。
Message rate out 表示消息出去的速率。

点击下方的 Add a new exchange 可以创建一个新的交换机。

  1. Queue

这个选项卡就是用来展示消息队列的:

各项含义如下:

  • Name:表示消息队列名称。
  • Type:表示消息队列的类型,除了上图的 classic,另外还有一种消息类型是 Quorum。两个区别如下图:

  • Features:表示消息队列的特性,D 表示消息队列持久化。
  • State:表示当前队列的状态,running 表示运行中;idle 表示空闲。
  • Ready:表示待消费的消息总数。
  • Unacked:表示待应答的消息总数。
  • Total:表示消息总数 Ready+Unacked。
  • incoming:表示消息进入的速率。
  • deliver/get:表示获取消息的速率。
  • ack:表示消息应答的速率。

点击下方的 Add a new queue 可以添加一个新的消息队列。

点击每一个消息队列的名称,可以进入到消息队列中。进入到消息队列后,可以完成对消息队列的进一步操作,例如:

  • 将消息队列和某一个交换机进行绑定。
  • 发送消息。
  • 获取一条消息。
  • 移动一条消息(需要插件的支持)。
  • 删除消息队列。
  • 清空消息队列中的消息。
  • …

如下图:

  1. Admin

这里是做一些用户管理操作,如下图:

各项属性含义如下:

  • Name:表示用户名称。
  • Tags:表示角色标签,只能选取一个。
  • Can access virtual hosts:表示允许进入的虚拟主机。
  • Has password:表示这个用户是否设置了密码。

常见的两个操作时管理用户和虚拟主机。

点击下方的 Add a user 可以添加一个新的用户,添加用户的时候需要给用户设置 Tags,其实就是用户角色,如下:

  • none:
    不能访问 management plugin
  • management:
    用户可以通过 AMQP 做的任何事
    列出自己可以通过 AMQP 登入的 virtual hosts
    查看自己的 virtual hosts 中的 queues, exchanges 和 bindings
    查看和关闭自己的 channels 和 connections
    查看有关自己的 virtual hosts 的“全局”的统计信息,包含其他用户在这些 virtual hosts 中的活动
  • policymaker:
    management 可以做的任何事
    查看、创建和删除自己的 virtual hosts 所属的 policies 和 parameters
  • monitoring:
    management 可以做的任何事
    列出所有 virtual hosts,包括他们不能登录的 virtual hosts
    查看其他用户的 connections 和 channels
    查看节点级别的数据如 clustering 和 memory 使用情况
    查看真正的关于所有 virtual hosts 的全局的统计信息
  • administrator:
    policymaker 和 monitoring 可以做的任何事
    创建和删除 virtual hosts
    查看、创建和删除 users
    查看创建和删除 permissions
    关闭其他用户的 connections
  • impersonator(模拟者)
    模拟者,无法登录管理控制台。

另外,这里也可以进行虚拟主机 virtual host 的操作,不过关于虚拟主机我打算另外写一篇文章和大家详聊,这里就先不展开啦。

  1. 小结

好啦,今天算是一篇入门文章,和大家简单聊一聊 RabbitMQ 的 web 管理页面展示的一些信息。

本文转载自: 掘金

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

Java中抽象类和接口的区别

发表于 2021-11-23

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

🌊 作者主页:海拥

🌊 作者简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员

🌊 粉丝福利:粉丝群 每周送六本书,不定期送各种小礼品

抽象类 vs 接口

  1. 方法类型: 接口只能有抽象方法。抽象类可以有抽象和非抽象方法。从 Java 8 开始,它也可以有默认和静态方法。
  2. 最终变量: 在 Java 接口中声明的变量默认是最终的。抽象类可能包含非最终变量。
  3. 变量类型: 抽象类可以有final、non-final、静态和非静态变量。接口只有静态和最终变量。
  4. 实现: 抽象类可以提供接口的实现。接口不能提供抽象类的实现。
  5. 继承 vs 抽象: Java 接口可以使用关键字“implements”来实现,抽象类可以使用关键字“extends”进行扩展。
  6. 多重实现: 一个接口只能扩展另一个Java接口,一个抽象类可以扩展另一个Java类并实现多个Java接口。
  7. 数据成员的可访问性: 默认情况下,Java 接口的成员是公共的。Java 抽象类可以具有私有、受保护等类成员。
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
java复制代码import java.io.*;
abstract class Shape {
String objectName = " ";
Shape(String name) { this.objectName = name;
}
public void moveTo(int x, int y){
System.out.println(this.objectName + " "
+ "已移至"
+ " x = " + x + " and y = " + y);
}
abstract public double area();
abstract public void draw();
}

class Rectangle extends Shape {
int length, width;
Rectangle(int length, int width, String name){
super(name);
this.length = length;
this.width = width;
}
@Override public void draw(){
System.out.println("矩形已绘制");
}
@Override public double area(){
return (double)(length * width);
}
}

class Circle extends Shape {
double pi = 3.14;
int radius;
Circle(int radius, String name){
super(name);
this.radius = radius;
}
@Override public void draw(){
System.out.println("圆形已绘制");
}
@Override public double area(){
return (double)((pi * radius * radius) / 2);
}
}

class HY {
public static void main(String[] args){
Shape rect = new Rectangle(2, 3, "Rectangle");
System.out.println("矩形面积:"
+ rect.area());
rect.moveTo(1, 2);
System.out.println(" ");
Shape circle = new Circle(2, "Circle");
System.out.println("圆的面积:"
+ circle.area());
circle.moveTo(2, 4);
}
}

输出

1
2
3
4
5
java复制代码矩形面积:6.0
矩形已移至 x = 1 和 y = 2

圆的面积:6.28
圆已移至 x = 2 和 y = 4

如果您在矩形和圆形之间没有任何通用代码,请使用界面。

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复制代码import java.io.*;
interface Shape {
void draw();
double area();
}

class Rectangle implements Shape {
int length, width;
Rectangle(int length, int width){
this.length = length;
this.width = width;
}

@Override public void draw(){
System.out.println("矩形已绘制");
}

@Override public double area(){
return (double)(length * width);
}
}

class Circle implements Shape {
double pi = 3.14;
int radius;
Circle(int radius) { this.radius = radius; }

@Override public void draw(){
System.out.println("圆形已绘制");
}

@Override public double area(){
return (double)((pi * radius * radius) / 2);
}
}

class HY {
public static void main(String[] args){
Shape rect = new Rectangle(2, 3);
System.out.println("矩形面积:"
+ rect.area());
Shape circle = new Circle(2);
System.out.println("圆的面积:"
+ circle.area());
}
}

输出

1
2
java复制代码矩形面积:6.0
圆的面积:6.28

什么时候用什么?

如果以下任何陈述适用于您的情况,请考虑使用抽象类:

  • 在java应用程序中,有一些相关的类需要共享一些代码行,那么你可以将这些代码行放在抽象类中,并且这个抽象类应该由所有这些相关类进行扩展。
  • 您可以在抽象类中定义非静态或非最终字段,以便您可以通过方法访问和修改它们所属的对象的状态。
  • 您可以期望扩展抽象类的类具有许多公共方法或字段,或者需要除 public 之外的访问修饰符(例如 protected 和 private)。

如果以下任何陈述适用于您的情况,请考虑使用接口:

  • 它是一个完全的抽象,接口中声明的所有方法都必须由实现此接口的类来实现。
  • 一个类可以实现多个接口。它被称为多重继承。
  • 您想指定特定数据类型的行为,但不关心谁实现其行为。

本文转载自: 掘金

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

Xv6 Lab system calls Lab sys

发表于 2021-11-23

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

0072Vf1pgy1foxloigdl2j31kw0w0kib.jpg

Lab: system calls

From: pdos.csail.mit.edu/6.S081/2020…

【总结】编写系统调用的一般步骤

先给出在 Xv6 中编写系统调用的方法。

如果我们要在 Xv6 中添加一个系统,调用,比如叫做 xxx:

  1. add a prototype for the system call to user/user.h
1
c复制代码int xxx(int)
  1. add a stub to user/usys.pl
1
perl复制代码entry("xxx");
  1. add a syscall number to kernel/syscall.h
1
c复制代码#define SYS_xxx  22
  1. add the new syscall into kernel/syscall.c
1
2
3
4
5
6
c复制代码extern uint64 sys_xxx(void);  // 1

static uint64 (*syscalls[])(void) = {
...
[SYS_xxx] sys_xxx, // 2
};
  1. add sys_xxx (a function takes void as argument and returns uint64) inkernel/sysproc.c. This function do fetch arguments about the system call and return values.
1
2
3
4
5
c复制代码uint64 
sys_xxx(void)
{
// about arguments: the [xv6 book](https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf) Sections 4.3 and 4.4 of Chapter 4
}
  1. implement the syscall somewhere in the kernel. (e.g. implement a xxx function , defines in kernel/defs.h, to do hard work)

下面再做这个 Lab 的题目:

System call tracing

In this assignment you will add a system call tracing feature that may help you when debugging later labs. You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call’s number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments. The tracesystem call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.

这个很简单,直接上就行了。

Main implement

kernel/sysproc.c:

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

// *
uint64
sys_trace(void)
{
if (argint(0, &myproc()->tracemask) < 0)
return -1;

return 0;
}

kernel/proc.h:

1
2
3
4
5
6
7
c复制代码struct proc {
struct spinlock lock;
...
int pid; // Process ID
...
int tracemask; // trace mask // *
};

kernel/syscall.c:

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
c复制代码...

extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
...
extern uint64 sys_trace(void); // *

static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
...
[SYS_trace] sys_trace, // *
};

char *syscallnames[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
...
[SYS_trace] "trace", // *
};

void
syscall(void)
{
...

// trace // *
if (p->tracemask >> num) {
printf("%d: syscall %s -> %d\n",
p->pid, syscallnames[num], p->trapframe->a0);
}
}

Detailed diff

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
diff复制代码diff --git a/Makefile b/Makefile
index f0beb51..1c07efd 100644
--- a/Makefile
+++ b/Makefile
@@ -149,6 +149,7 @@ UPROGS=\
$U/_grind\
$U/_wc\
$U/_zombie\
+ $U/_trace\



diff --git a/gradelib.pyc b/gradelib.pyc
new file mode 100644
index 0000000..d118849
Binary files /dev/null and b/gradelib.pyc differ
diff --git a/kernel/proc.c b/kernel/proc.c
index 6afafa1..73845e4 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -280,6 +280,9 @@ fork(void)
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);

+ // copy trace mask
+ np->tracemask = p->tracemask;
+
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;

diff --git a/kernel/proc.h b/kernel/proc.h
index 9c16ea7..cf18b9b 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -103,4 +103,6 @@ struct proc {
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
+
+ int tracemask; // trace mask
};
diff --git a/kernel/syscall.c b/kernel/syscall.c
index c1b3670..182ca99 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -104,6 +104,7 @@ extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
+extern uint64 sys_trace(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
@@ -127,6 +128,32 @@ static uint64 (*syscalls[])(void) = {
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
+[SYS_trace] sys_trace,
+};
+
+char *syscallnames[] = {
+[SYS_fork] "fork",
+[SYS_exit] "exit",
+[SYS_wait] "wait",
+[SYS_pipe] "pipe",
+[SYS_read] "read",
+[SYS_kill] "kill",
+[SYS_exec] "exec",
+[SYS_fstat] "fstat",
+[SYS_chdir] "chdir",
+[SYS_dup] "dup",
+[SYS_getpid] "getpid",
+[SYS_sbrk] "sbrk",
+[SYS_sleep] "sleep",
+[SYS_uptime] "uptime",
+[SYS_open] "open",
+[SYS_write] "write",
+[SYS_mknod] "mknod",
+[SYS_unlink] "unlink",
+[SYS_link] "link",
+[SYS_mkdir] "mkdir",
+[SYS_close] "close",
+[SYS_trace] "trace",
};

void
@@ -143,4 +170,10 @@ syscall(void)
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
+
+ // trace
+ if (p->tracemask >> num) {
+ printf("%d: syscall %s -> %d\n",
+ p->pid, syscallnames[num], p->trapframe->a0);
+ }
}
diff --git a/kernel/syscall.h b/kernel/syscall.h
index bc5f356..01004db 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -20,3 +20,5 @@
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
+
+#define SYS_trace 22
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index e8bcda9..7a078c0 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -95,3 +95,12 @@ sys_uptime(void)
release(&tickslock);
return xticks;
}
+
+uint64
+sys_trace(void)
+{
+ if (argint(0, &myproc()->tracemask) < 0)
+ return -1;
+
+ return 0;
+}
diff --git a/user/user.h b/user/user.h
index b71ecda..774bb03 100644
--- a/user/user.h
+++ b/user/user.h
@@ -24,6 +24,8 @@ char* sbrk(int);
int sleep(int);
int uptime(void);

+int trace(int);
+
// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..2e51985 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,5 @@ entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
+
+entry("trace");

Sysinfo

In this assignment you will add a system call, sysinfo, that collects information about the running system. The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h). The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED. We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

也是照着题目写即可。

Main implement

kernel/sysproc.c:

1
2
3
4
5
6
7
8
9
c复制代码uint64
sys_sysinfo(void)
{
uint64 info; // user pointer to struct stat

if(argaddr(0, &info) < 0)
return -1;
return systeminfo(info);
}

kernel/sysinfo.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码#include "types.h"
#include "riscv.h"
#include "param.h"
#include "spinlock.h"
#include "defs.h"
#include "sysinfo.h"
#include "proc.h"

// Get current system info
// addr is a user virtual address, pointing to a struct sysinfo.
int
systeminfo(uint64 addr) {
struct proc *p = myproc();
struct sysinfo info;

info.freemem = freemem();
info.nproc = nproc();

if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}

kernel/proc.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码// Get the number of processes whose state is not UNUSED. 
int
nproc(void)
{
int n = 0;
struct proc *p;

for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED)
++n;
release(&p->lock);
}

return n;
}

kernel/kalloc.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码// Get the number of bytes of free memory
int
freemem(void)
{
int n = 0;
struct run *r;
acquire(&kmem.lock);

for (r = kmem.freelist; r; r = r->next)
++n;

release(&kmem.lock);

return n * 4096;
}

Detailed diff

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
diff复制代码diff --git a/Makefile b/Makefile
index 1c07efd..b626677 100644
--- a/Makefile
+++ b/Makefile
@@ -36,6 +36,7 @@ OBJS = \
$K/kernelvec.o \
$K/plic.o \
$K/virtio_disk.o \
+ $K/sysinfo.o \

ifeq ($(LAB),pgtbl)
OBJS += $K/vmcopyin.o
@@ -150,6 +151,7 @@ UPROGS=\
$U/_wc\
$U/_zombie\
$U/_trace\
+ $U/_sysinfotest\



diff --git a/kernel/defs.h b/kernel/defs.h
index 4b9bbc0..7ebebb8 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -8,6 +8,7 @@ struct spinlock;
struct sleeplock;
struct stat;
struct superblock;
+struct sysinfo;

// bio.c
void binit(void);
@@ -63,6 +64,7 @@ void ramdiskrw(struct buf*);
void* kalloc(void);
void kfree(void *);
void kinit(void);
+int freemem(void);

// log.c
void initlog(int, struct superblock*);
@@ -104,6 +106,7 @@ void yield(void);
int either_copyout(int user_dst, uint64 dst, void *src, uint64 len);
int either_copyin(void *dst, int user_src, uint64 src, uint64 len);
void procdump(void);
+int nproc(void);

// swtch.S
void swtch(struct context*, struct context*);
@@ -183,5 +186,8 @@ void virtio_disk_init(void);
void virtio_disk_rw(struct buf *, int);
void virtio_disk_intr(void);

+// sysinfo.c
+int systeminfo(uint64);
+
// number of elements in fixed-size array
#define NELEM(x) (sizeof(x)/sizeof((x)[0]))
diff --git a/kernel/kalloc.c b/kernel/kalloc.c
index fa6a0ac..5051a1d 100644
--- a/kernel/kalloc.c
+++ b/kernel/kalloc.c
@@ -80,3 +80,19 @@ kalloc(void)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
+
+// Get the number of bytes of free memory
+int
+freemem(void)
+{
+ int n = 0;
+ struct run *r;
+ acquire(&kmem.lock);
+
+ for (r = kmem.freelist; r; r = r->next)
+ ++n;
+
+ release(&kmem.lock);
+
+ return n * 4096;
+}
diff --git a/kernel/proc.c b/kernel/proc.c
index 73845e4..73d2745 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -696,3 +696,20 @@ procdump(void)
printf("\n");
}
}
+
+// Get the number of processes whose state is not UNUSED.
+int
+nproc(void)
+{
+ int n = 0;
+ struct proc *p;
+
+ for(p = proc; p < &proc[NPROC]; p++) {
+ acquire(&p->lock);
+ if(p->state != UNUSED)
+ ++n;
+ release(&p->lock);
+ }
+
+ return n;
+}
diff --git a/kernel/syscall.c b/kernel/syscall.c
index 182ca99..fdf996d 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -105,6 +105,7 @@ extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);
+extern uint64 sys_sysinfo(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
@@ -129,6 +130,7 @@ static uint64 (*syscalls[])(void) = {
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
[SYS_trace] sys_trace,
+[SYS_sysinfo] sys_sysinfo,
};

char *syscallnames[] = {
@@ -154,6 +156,7 @@ char *syscallnames[] = {
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
+[SYS_sysinfo] "sysinfo",
};

void
diff --git a/kernel/syscall.h b/kernel/syscall.h
index 01004db..6716e88 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -22,3 +22,4 @@
#define SYS_close 21

#define SYS_trace 22
+#define SYS_sysinfo 23
diff --git a/kernel/sysinfo.c b/kernel/sysinfo.c
new file mode 100644
index 0000000..40bd08a
--- /dev/null
+++ b/kernel/sysinfo.c
@@ -0,0 +1,22 @@
+#include "types.h"
+#include "riscv.h"
+#include "param.h"
+#include "spinlock.h"
+#include "defs.h"
+#include "sysinfo.h"
+#include "proc.h"
+
+// Get current system info
+// addr is a user virtual address, pointing to a struct sysinfo.
+int
+systeminfo(uint64 addr) {
+ struct proc *p = myproc();
+ struct sysinfo info;
+
+ info.freemem = freemem();
+ info.nproc = nproc();
+
+ if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
+ return -1;
+ return 0;
+}
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 7a078c0..a1231d0 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -104,3 +104,13 @@ sys_trace(void)

return 0;
}
+
+uint64
+sys_sysinfo(void)
+{
+ uint64 info; // user pointer to struct stat
+
+ if(argaddr(0, &info) < 0)
+ return -1;
+ return systeminfo(info);
+}
diff --git a/time.txt b/time.txt
new file mode 100644
index 0000000..0cfbf08
--- /dev/null
+++ b/time.txt
@@ -0,0 +1 @@
+2
diff --git a/user/user.h b/user/user.h
index 774bb03..708496d 100644
--- a/user/user.h
+++ b/user/user.h
@@ -1,5 +1,6 @@
struct stat;
struct rtcdate;
+struct sysinfo;

// system calls
int fork(void);
@@ -25,6 +26,7 @@ int sleep(int);
int uptime(void);

int trace(int);
+int sysinfo(struct sysinfo *);

// ulib.c
int stat(const char*, struct stat*);
diff --git a/user/usys.pl b/user/usys.pl
index 2e51985..fce5771 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -38,3 +38,4 @@ entry("sleep");
entry("uptime");

entry("trace");
+entry("sysinfo");

EOF


By CDFMLR

顶部图片来自于网络,系随机选取的图片,仅用于检测屏幕显示的机械、光电性能,与文章的任何内容及观点无关,也并不代表本人局部或全部同意、支持或者反对其中的任何内容及观点。如有侵权,联系删除。

本文转载自: 掘金

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

Location规则介绍

发表于 2021-11-23

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

location修饰符类型

「=」 修饰符:要求路径完全匹配

1
2
3
4
5
6
ini复制代码server {
server_name website.com;
location = /abcd {
[…]
}
}

website.com/abcd匹配

website.com/ABCD可能会匹配 ,也可以不匹配,取决于操作系统的文件系统是否大小写敏感(case-sensitive)。ps: Mac 默认是大小写不敏感的,git 使用会有大坑。

website.com/abcd?param1…,忽略 querystring

website.com/abcd/不匹配,带有结尾的

website.com/abcde不匹配

「~」修饰符:区分大小写的正则匹配

1
2
3
4
5
6
ini复制代码server {
server_name website.com;
location ~ ^/abcd$ {
[…]
}
}

^/abcd这个正则表达式表示字符串必须以/开始,以这个正则表达式表示字符串必须以/开始,以这个正则表达式表示字符串必须以/开始,以结束,中间必须是abcd

website.com/abcd匹配(完全匹配)

website.com/ABCD不匹配,大小写敏感

website.com/abcd?param1…

website.com/abcd/不匹配,不能匹配正则表达式

website.com/abcde不匹配,不能匹配正则表达式

「~*」不区分大小写的正则匹配

1
2
3
4
5
6
ini复制代码server {
server_name website.com;
location ~* ^/abcd$ {
[…]
}
}

website.com/abcd匹配 (完全匹配)

website.com/ABCD匹配 (大小写不敏感)

website.com/abcd?param1…

website.com/abcd/ 不匹配,不能匹配正则表达式

website.com/abcde 不匹配,不能匹配正则表达式

修饰符:前缀匹配

如果该 location 是最佳的匹配,那么对于匹配这个 location 的字符串, 该修饰符不再进行正则表达式检测。注意,这不是一个正则表达式匹配,它的目的是优先于正则表达式的匹配

查找的顺序及优先级

当有多条 location 规则时,nginx 有一套比较复杂的规则,优先级如下:

精确匹配 =

前缀匹配 ^~(立刻停止后续的正则搜索)

按文件中顺序的正则匹配 或*

匹配不带任何修饰的前缀匹配。

这个规则大体的思路是

先精确匹配,没有则查找带有 ^~的前缀匹配,没有则进行正则匹配,最后才返回前缀匹配的结果(如果有的话)

如果上述规则不好理解,可以看下面的伪代码(非常重要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码function match(uri):
rv = NULL

if uri in exact_match:

return exact_match[uri]

if uri in prefix_match:

if prefix_match[uri] is '^~':
return prefix_match[uri]
else:
rv = prefix_match[uri] // 注意这里没有 return,且这里是最长匹配

if uri in regex_match:

return regex_match[uri] // 按文件中顺序,找到即返回

return rv

一个简化过的Node.js写的代码如下

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
ini复制代码function ngx_http_core_find_location(uri, static_locations, regex_locations, named_locations, track) {
let rc = null;
let l = ngx_http_find_static_location(uri, static_locations, track);\
if (l) {
if (l.exact_match) {
return l;
}
if (l.noregex) {
return l;
}
rc = l;
}
if (regex_locations) {
for (let i = 0 ; i < regex_locations.length; i ++) {
if (track) track(regex_locations[i].id);
let n = null;
if (regex_locations[i].rcaseless) {
n = uri.match(new RegExp(regex_locations[i].name));
} else {
n = uri.match(new RegExp(regex_locations[i].name), "i");
}
if (n) {
return regex_locations[i];
}
}
}
return rc;
}
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码server {
server_name website.com;
location /doc {
return 701; # 用这样的方式,可以方便的知道请求到了哪里
}
location ~* ^/document$ {
return 702; # 用这样的方式,可以方便的知道请求到了哪里

}
}

curl -I website.com:8080/document

按照上述的规则,第二个会有更高的优先级

1
2
3
4
5
6
7
8
9
bash复制代码server {
server_name website.com;
location /document {
return 701;
}
location ~* ^/document$ {
return 702;
}
}

curl -I website.com:8080/document

第二个匹配了正则表达式,优先级高于第一个普通前缀匹配

案例 3

1
2
3
4
5
6
7
8
9
bash复制代码server {
server_name website.com;
location ^~ /doc {
return 701;
}
location ~* ^/document$ {
return 702;
}
}

curl website.com/document
HTTP/1.1 701
第一个前缀匹配^~命中以后不会再搜寻正则匹配,所以会第一个命中

案例 4

1
2
3
4
5
6
7
8
9
bash复制代码server {
server_name website.com;
location /docu {
return 701;
}
location /doc {
return 702;
}
}

curl -I website.com:8080/document 返回 HTTP/1.1 701,

1
2
3
4
5
6
7
8
9
bash复制代码server {
server_name website.com;
location /doc {
return 702;
}
location /docu {
return 701;
}
}

curl -I website.com:8080/document 依然返回 HTTP/1.1 701

前缀匹配下,返回最长匹配的 location,与 location 所在位置顺序无关

案例 5

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码server {
listen 8080;
server_name website.com;

location ~ ^/doc[a-z]+ {
return 701;
}

location ~ ^/docu[a-z]+ {
return 702;
}
}

curl -I website.com:8080/document 返回 HTTP/1.1 701

把顺序换一下

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码server {
listen 8080;
server_name website.com;

location ~ ^/docu[a-z]+ {
return 702;
}

location ~ ^/doc[a-z]+ {
return 701;
}
}

curl -I website.com:8080/document 返回 HTTP/1.1 702

正则匹配是使用文件中的顺序,找到返回

Linux 监控搭建方案

grafana+prometheus+node_exporter+mysql_exporter+redis_exporter

具体细节百度

NGINX directory index of “” is forbidden

1
csharp复制代码directory index of "" is forbidden

一直以为是权限问题,排查半天。

1
2
3
4
5
bash复制代码location / {
index index.php;
try_files $uri $uri/ /index.php$uri?$query_string;
break;
}

最后发现 index index.php 没写

DOCKER 权限不足

docker run -it –privileged -v /Users/liubb/html:/home/wwwroot -p 50007:80 1e1 /usr/sbin/init

关键:

privileged

/usr/sbin/init

本文转载自: 掘金

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

性能监控之Telegraf+InfluxDB+Grafana

发表于 2021-11-23

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

一、背景

性能测试需要监控服务端 JVM 信息,Java 虚拟机 (JVM) 提供操作管理和监测提供了一套完整框架,即 JMX(Java 管理扩展),我们需要做到采集其所暴露出来的性能指标。

二、什么是JMX?

JMX 技术定义了完整的架构和设计模式集合,以对 Java 应用进行监测和管理。JMX 的基础是托管豆(managed bean,业内更习惯将其称为 MBean),MBean 是通过依赖注入完成实例化的各个类别,代表着 JVM 中的资源。由于 MBean 代表 JVM 中的资源,所以我们可以用其来管理应用的特定方面,或者更为常见的一种做法,用其来收集与这些资源的使用相关的统计数据。JMX 的核心是 MBean 服务器,此类服务器可以作为媒介将 MBean、同一 JVM 内的应用以及外部世界联系在一起。与 MBean 之间的任何交互都是通过此服务器完成的。通常而言,只有 Java 代码能够直接访问 JMX API,但是有一些适配器可将该 API 转换为标准协议,例如 Jolokia 便可将其转换为 HTTP。

三、什么是Jolokia?

Jolokia 作为目前最主流的 JMX 监控组件,spring 社区(springboot、MVC、cloud)以及目前主流的中间件服务均采用它作为 JMX 监控,Jolokia 是无类型的数据,使用了 Json 这种轻量化的序列化方案来替代 RMI 方案。

四、Jolokia的特点?

  • JMX 可以实现 VM 内部运行时数据状态的对外 export,我们通过将运行态数据封装成 MBean,通过 JMX Server 统一管理,并允许外部程序通过 RMI 方式获取数据。总之,JMX允许运行态数据通过 RMI 协议被外部程序获取。这对我们监控、操作 VM 内部数据提供窗口。
  • JMX 扩展性、可实施能力非常强大,但是其问题就是如果获取 MBean 数据,需要使用 JAVA 栈的 RMI 协议,这对外部程序比如监控组件(非JAVA栈)支持不够良好。
  • Jolokia 完全兼容并支撑 JMX 组件,它可以作为 agent 嵌入到任何 JAVA 程序中,特别是 WEB 应用,它将复杂而且难以理解的 MBean Filter 查询语句,转换成更易于实施和操作的 HTTP 请求范式,不仅屏蔽了 RMI 的开发困难问题,还实现了对外部监控组件的透明度,而且更易于测试和使用。
  • 直观来说,Jolokia 就是用于解决 JMX 数据获取时,所遇到的 RMI 协议复杂性、Mbean 查询的不便捷、数据库序列化、MBeanServer 的托管等问题
  • 我们只需要使用 HTTP 请求,直接访问与 WEB 服务相同的 port 即可获取 JMX 数据。

五、选型考虑

  • 由于在服务端在集群化的弹性环境中,考虑未来微服务下节点大量增长、扩展,并由非常多的应用实例所组成。对于单独节点的监控可能即费力又没有什么实际效果。所以,使用基于时间序列的数据聚合方式将获得更好的效果。
  • Spring Boot & Spring MVC 认可使用 Jolokia 来通过 HTTP 导出 export JMX 数据。只需要在工程类路径中增加一些依赖项,一切都是开箱即用的。不需要任何额外的实现。
  • Telegraf 支持通过整合 Jolokia 来集成 JMX 数据的收集。它有一个预制的输入插件,它是开箱即用的。不需要任何额外的实现。只需要做一些配置即可。
  • InfluxDB 通过输出插件从 Telegraf 接收指标数据,它是开箱即用的,不需要任何额外的实现。
  • Grafana 通过连接 InfluxDB 作为数据源来渲染 Dashboard。它是开箱即用的,不需要额外的实现。

在这里插入图片描述

六、Jolokia & 服务端集成

1、Jolokia Agent模式

Agent 可以调用本地的 MBeanServer 暴露 Restful 接口供外部调用,在客户端上可以应用不同的技术来展示通过 Http 获取的 JMX 数据
在这里插入图片描述

Agent模式主要有以下的方式:

  • 方法一:是将 jolokia 放置到 servlet 容器中,比如 Tomcat 或 Jetty,这样 Jolokia 完全可以看做是一个常规的 Java web 应用,让所有的开发人员都能够很好理解并快速的从中读取数据,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码[root@localhost webapps]# pwd
/usr/local/src/apache-tomcat-7.0.73/webapps
[root@localhost webapps]# ls -l
total 83428
drwxr-xr-x 14 root root 4096 Jun 28 2018 docs
drwxr-xr-x 7 root root 105 Jun 28 2018 examples
drwxr-xr-x 5 root root 82 Jun 28 2018 host-manager
drwxr-xr-x 4 root root 35 Apr 5 2019 jolokia
-rw-r--r-- 1 root root 307617 Nov 9 2014 jolokia.war
drwxr-xr-x 5 root root 97 Jun 28 2018 manager
drwxr-xr-x 3 root root 4096 Jun 28 2018 ROOT
drwxr-xr-x 3 root root 22 Mar 22 2019 v3cAPITestReport
drwxr-xr-x 7 root root 4096 Jun 28 2018 visu1021
-rw-r--r-- 1 root root 85103469 Jun 28 2018 visu1021.war

在这里插入图片描述

  • 方法二: 除了放到 Servlet 容器之外,Jolokia 也可以定义特殊的 Agent,比如实现 OSGi 或者内置 Jetty 服务器
  • 方法三:Jolokia 也可以集成到 Web 应用中,jolokia-core 库作为一个 Jar 包,提供一个 Servlet,加入到 Web 应用中之后就可以访问。

考虑到集群部署及目前服务端现状,推荐第三种方式。

2、jolokia-core 集成示例

2.1、SpringMVC

主要步骤:

  1. pom.xml 中增加 jolokia 依赖。
  2. 在 web.xml 中声明 jolokia servlet 启动和适配。
  3. 在 resources 目录下增加 jolokia-access.xml 安全访问
  4. 在 spring xml 文件中增加相关MBean export显示操作。

我们需要在 pom.xml 中增加 jolokia 的依赖,使用最新版本。

1
2
3
4
5
6
xml复制代码<!-- jolokia 核心组件 -->
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
<version>1.6.1</version>
</dependency>

需要注意,jolokia 作为嵌入式 agent,将会与我们 web 容器一起启动,jolokia agent 与 web 服务共享一个 HTTP 端口,由此 servlet 负责承担请求解析。此后可以通过 “/jolokia” 来访问内部的 JMX 数据

1
2
3
4
5
6
7
8
9
10
xml复制代码   <!-- jolokia 监控 -->
<servlet>
<servlet-name>jolokia‐agent</servlet-name>
<servlet-class>org.jolokia.http.AgentServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jolokia‐agent</servlet-name>
<url-pattern>/jolokia/*</url-pattern>
</servlet-mapping>

jolokia 以 servlet 服务提供给外部程序,那么意味着我们可以通过 URL 获取数据,在很多时候我们不希望这些数据被外部非法用户获取、只对内部监控组件开发,比如不希望用户通过 “域名 + /jolokia” 来获取数据等。此 jolokia-access.xml 表示,只允许 "127.0.0.1" 即本地的监控组件可以获取数据,对于跨机器、代理程序均无法获取。这也要求我们的 telegraf 探针是部署在WEB应用的宿主机器上。

1
2
3
4
5
xml复制代码<?xml version="1.0" encoding="UTF-8"?>  
<restrict>
<!-- 若需限制请自行配置 -->
<!-- <host>127.0.0.1</host> -->
</restrict>

此后,我们将也可以通过如下 http 接口查看 jolokia 的是否正常
在这里插入图片描述

2.2、SpringBoot

Springboot 项目,对 endpoint 管理更加智能化和全面,jmx 的支持很封装也更加完善,所以实现 jmx 监控更加便捷。(建议关注acturator组件)

主要步骤:

  1. pom.xml 中增加 acturator 组件的引入,包括 jolokia 组件。
  2. 利用 springboot 自动装配,我们开启 “management”、“endpoint” 功能即可。
  3. 我们不再需要 web.xml 以及 jolokia-access.xml ,因为这些都是默认支持的。(自动装配)
1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- jolokia 核心组件 -->
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
<version>1.6.1</version>
</dependency>
1
2
3
4
5
6
7
8
yml复制代码#jolokia  
management:
security:
enabled: false
address: 127.0.0.1
endpoints:
jolokia:
enabled: true

我们在 application.yml 文件中增加相应的配置,特别注意 management 和 endpoint 部分。

七、Telegraf 配置

Telegraf 的 Jolokia2 输入插件支持使用 JSON-over-HTTP 协议从一个或多个Jolokia代理REST端点读取JMX指标数据。

1
2
3
4
5
6
7
bash复制代码[[inputs.jolokia2_agent]]
urls = ["http://agent:8080/jolokia"]

[[inputs.jolokia2_agent.metric]]
name = "jvm_runtime"
mbean = "java.lang:type=Runtime"
paths = ["Uptime"]

每个度量声明生成一个 Jolokia 请求,以便从 JMX MBean 获取指标数据。

Key Required Description
mbean yes JMX MBean 的对象名。MBean 属性键值可以包含通配符 *,允许通过一个声明获取多个 MBean
paths no 要读取的 MBean 属性列表。
tag_keys no 要转换为标记的 MBean 属性键名称列表。属性键名成为标记名,而属性键值成为标记值。
tag_prefix no 在此指标声明生成的标记名称之前的字符串。
field_name no 要设置为由该度量生成的字段的名称的字符串;可以替换。
field_prefix no 在此指标声明产生的字段名之前的字符串;可以替换。

JVM配置如下:

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
bash复制代码# # Read JMX metrics from a Jolokia REST agent endpoint
[[inputs.jolokia2_agent]]
urls = ["http://localhost:8089/jolokia"]

[[inputs.jolokia2_agent.metric]]
name = "java_runtime"
mbean = "java.lang:type=Runtime"
paths = ["Uptime"]

[[inputs.jolokia2_agent.metric]]
name = "java_memory"
mbean = "java.lang:type=Memory"
paths = ["HeapMemoryUsage", "NonHeapMemoryUsage", "ObjectPendingFinalizationCount"]

[[inputs.jolokia2_agent.metric]]
name = "java_garbage_collector"
mbean = "java.lang:name=*,type=GarbageCollector"
paths = ["CollectionTime", "CollectionCount"]
tag_keys = ["name"]

[[inputs.jolokia2_agent.metric]]
name = "java_last_garbage_collection"
mbean = "java.lang:name=*,type=GarbageCollector"
paths = ["LastGcInfo"]
tag_keys = ["name"]

[[inputs.jolokia2_agent.metrics]]
name = "java_threading"
mbean = "java.lang:type=Threading"
paths = ["TotalStartedThreadCount", "ThreadCount", "DaemonThreadCount", "PeakThreadCount"]

[[inputs.jolokia2_agent.metrics]]
name = "java_class_loading"
mbean = "java.lang:type=ClassLoading"
paths = ["LoadedClassCount", "UnloadedClassCount", "TotalLoadedClassCount"]

[[inputs.jolokia2_agent.metrics]]
name = "java_memory_pool"
mbean = "java.lang:name=*,type=MemoryPool"
paths = ["Usage", "PeakUsage", "CollectionUsage"]
tag_keys = ["name"]

其他配置可以参考官方示例:
github.com/influxdata/…

InfluxDB 采集的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码> show measurements
name: measurements
name
----
java_class_loading
java_garbage_collector
java_memory
java_memory_pool
java_runtime
java_threading
> select * from java_memory limit 5
name: java_memory
time HeapMemoryUsage.committed HeapMemoryUsage.init HeapMemoryUsage.max HeapMemoryUsage.used NonHeapMemoryUsage.committed NonHeapMemoryUsage.init NonHeapMemoryUsage.max NonHeapMemoryUsage.used ObjectPendingFinalizationCount host jolokia_agent_url
---- ------------------------- -------------------- ------------------- -------------------- ---------------------------- ----------------------- ---------------------- ----------------------- ------------------------------ ---- -----------------
1571105290000000000 3344433152 2147483648 7635730432 1216965408 172425216 2555904 -1 169201160 0 DESKTOP-MLD0KTS http://localhost:8089/jolokia
1571105300000000000 3344433152 2147483648 7635730432 1287744120 172425216 2555904 -1 168805312 0 DESKTOP-MLD0KTS http://localhost:8089/jolokia
1571105310000000000 3344433152 2147483648 7635730432 1359803320 172425216 2555904 -1 168981192 0 DESKTOP-MLD0KTS http://localhost:8089/jolokia
1571105320000000000 3344433152 2147483648 7635730432 1434671784 172425216 2555904 -1 169114552 0 DESKTOP-MLD0KTS http://localhost:8089/jolokia
1571105330000000000 3344433152 2147483648 7635730432 1509156560 172425216 2555904 -1 169181064 0 DESKTOP-MLD0KTS http://localhost:8089/jolokia

八、Grafana监控效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

相关资料:

  • github.com/zuozewei/bl…

参考资料:

  • [1]:jolokia.org/reference/h…
  • [2]:shift-alt-ctrl.iteye.com/blog/240403…
  • [3]:blog.didispace.com/spring-boot…
  • [4]:jolokia.org/reference/h…
  • [5]:github.com/influxdata/…

本文转载自: 掘金

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

一文明白CDN加速是个啥

发表于 2021-11-23

作者:IT王小二

博客:itwxe.com

不知不觉三个月没更新了,这三个月诸事繁忙啊!最近没那么忙了,开始恢复更新。

一、CDN简介

CDN(Content Delivery Network)是指内容分发网络,也称为内容传送网络,这个概念始于1996年,是美国麻省理工学院的一个研究小组为改善互联网的服务质量而提出的。为了能在传统IP网上发布丰富的宽带媒体内容,他们提出在现有互联网基础上建立一个内容分发平台专门为网站提供服务,并于1999年成立了专门的CDN服务公司,为Yahoo提供专业服务。由于CDN是为加快网络访问速度而被优化的网络覆盖层,因此被形象地称为“网络加速器”。

CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。

CDN的基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。 –摘自百度百科

二、通俗易懂理解CDN

看了上面的话不知道 CDN 用来干啥的?不要紧,这不是有小二在吗。小二这就给小伙伴们娓娓道来。

先不说 CDN,相信小伙伴们都在京东自营店买过东西吧,要是没有的话扣 1 哈。

我信你个鬼

相信用过京东自营的小伙伴都知道,购买的商品基本隔天就到我们手中,这是为啥呢?

细心的小伙伴都可以看到发货地址都离自己很近,京东将商品存储在全国的八个大仓库里面,当我们从就京东自营店购买一个商品,那么京东会从离你最近的仓库发货,就近发货减少了道路的拥堵和长距离运输,基本隔天就能收到商品,速度非常的快,再也不用漫长的等待。

那么我们访问一个网站时,如果站点服务器是在广州,而访问的用户在北京,那么不同时间段的运营商网络状态和经过长距离的网络传输,访问速度会变慢,那么访问网站时有没有办法类似京东买商品可以快速到货呢,当然有,解决办法之一就是 CDN。

所以说人话, CDN 就是一个你的网站缓存, 而 CDN 节点就类似京东在全国的八大仓库,提前把网站内容存到了 CDN 节点,然后每个请求网站的用户从就近节点获取网站数据,大大减短了网络的长距离传输和网络拥堵的状况下的访问速度,类似下图的效果。

用户访问CDN

同时不同 CDN 服务商还会针对电信、移动、联通等网络专门设置 CDN 节点,减少网络服务商之间的切换。

三、CDN的好处

  • 隐藏服务器源ip。
  • 连接所响应速度最快的节点提高访问速度。
  • CDN节点缓存减少网站服务器访问压力。

四、CDN原理解析

因为小二是程序猿嘛,所以肯定要扯一扯 CDN 原理的。以我的域名 itwxe.com 为例看看一个请求走的解析路线,目前我的解析服务商是 DNSPod,CDN 使用的是百度云加速。

不使用 CDN 时

不使用CDN时

  1. 用户输入访问的域名 itwxe.com,操作系统向 LocalDNS 查询域名的 IP 地址。
  2. LocalDNS 向 RootDNS 查询域名的授权服务器 (这里假设 LocalDNS 缓存过期)。
  3. RootDNS 将域名授权 DNS 记录返回给 LocalDNS,我的是 DNSPod。
  4. LocalDNS 得到域名的授权 DNS 记录后,继续向域名授权 DNS (DNSPod)查询域名的 IP 地址。
  5. 域名授权 DNS (DNSPod)查询域名记录后,返回给 LocalDNS。
  6. LocalDNS 将得到的域名映射的 IP 地址,返回给用户端。
  7. 用户端得到域名 IP 地址后,访问站点服务器。
  8. 站点服务器接收请求,将内容返回给客户端。

使用 CDN 时

使用CDN时

  1. 用户输入访问的域名 itwxe.com,操作系统向 LocalDNS 查询域名的 IP 地址。
  2. LocalDNS 向 RootDNS 查询域名的授权服务器 (这里假设 LocalDNS 缓存过期)。
  3. RootDNS 将域名授权 DNS 记录返回给 LocalDNS,我的是 DNSPod。
  4. LocalDNS 得到域名的授权 DNS 记录后,继续向域名授权 DNS (DNSPod)查询域名的 IP 地址。
  5. 域名授权 DNS (DNSPod)查询域名记录后(一般是CNAME,例如我的是itwxe.com.cname.yunjiasu-cdn.net.),返回给 LocalDNS。
  6. LocalDNS 得到域名记录后,向智能调度 DNS 查询域名的 IP 地址。
  7. 智能调度 DNS 根据一定的算法和策略,将最适合的 CDN 节点 IP 地址返回给 LocalDNS。
  8. LocalDNS 将得到的域名 IP 地址,返回给用户端。
  9. 用户得到域名 IP 地址后,访问站点服务器。
  10. 站点服务器接收请求,将内容返回给客户端。

使用 dig 命令可以看到请求的 cname 地址和 最终访问的 CDN 节点 IP,如下图访问我的博客访问的节点。

image-20211122124106221

五、免费CDN服务商

有人说过:“免费的往往是最贵的”,所以…

下面列举的免费 CDN 使用时一定要注意免费版官方的使用说明,没有设置好 CDN 的情况下如果被 DDos 了可能一夜之间倾家荡产。

下面列举的免费 CDN 使用时一定要注意免费版官方的使用说明,没有设置好 CDN 的情况下如果被 DDos 了可能一夜之间倾家荡产。

下面列举的免费 CDN 使用时一定要注意免费版官方的使用说明,没有设置好 CDN 的情况下如果被 DDos 了可能一夜之间倾家荡产。

重要的事情说三遍,下面小二开始正式说说目前免费 CDN 有哪些比较好用的。

国外

  • Cloudflare

cloudflare

官方网站:www.cloudflare.com/zh-cn/

国外最好用的 CDN,没有之一,如果你的服务器在国外且域名没有备案,那么需要使用 CDN 的市况下 Cloudflare 可以说是最好的选择了,免费套餐即不限流量,还能抗 DDos 攻击。当然如果你的服务器在国内且域名已经备案了,那么就使用国内的 CDN 吧,就不是加速 CDN,而是减速 CDN 了。

**优点:**免费套餐即不限流量,可以抗 DDos 攻击。

**缺点:**在国内访问速度不佳。

国内

如果需要使用国内的 CDN 服务商服务,那么前提就是域名需要备案了,备案了才能使用国内的 CDN,下面介绍几个常见的。

  • 百度云加速

百度云加速

官方网站:su.baidu.com/

小二第一个使用的,也是目前正在使用的 CDN 服务商,几个月前还是 CDN 节点数量 6-9 个,流量10g/天,单个上传文件大小 100M,现在没想到都缩水这么多了,不过目前使用速度还可以,免费又大碗。

顺便说一下百度云 CDN,百度云 CDN 和百度云加速不是同一个产品,百度云 CDN 号称可以24小时极速收录,但是我试用了两个月的免费套餐之后还是切换回了百度云加速。原因是24小时极速收录效果不佳,我问了客服,客服说也只是把访问数据提交给收录部门进行处理。

**优点:**免费套餐缩水后流量5G/天,速度还不错。

**缺点:**随着免费套餐缩水,CDN节点数量减少,很多功能需要付费使用,同时个人认证需要手持身份证拍照(略麻烦)。

  • 奇安信CDN

官方网站:wangzhan.qianxin.com/

这个是最近百度云加速免费版套餐缩水和加了很多限制后小二朋友推荐给我的,小二后面会考虑 CDN 从百度云加速切换到奇安信 CDN。因为目前奇安信 CDN 免费不限量,朋友博客测试访问速度还可以,妈妈再也不用担心我会因为 DDos 攻击倾家荡产了。

**优点:**免费不限量,安全,不用担心 DDos。

**缺点:**节点少,同时个人认证也需要手持身份证认证(不过朋友说上传个奥特曼都行,只要你网站备案了,个人认证没人审核的)。

  • 又拍云

又拍云

官方网站:www.upyun.com/

也是很多博主正在使用的 CDN,申请加入又拍云联盟后将又拍云链接附在网站底部,每个月就可以获得 15GB 的 CDN 流量和 10GB 的存储空间(即对象存储),对象存储自动走 CDN 流量,无需其他配置。

**优点:**速度快,节点多,有在线客服。

缺点:流量少,又拍云节点经常被打,每年得申请一次又拍云联盟。

其他比较常见的还有 上海云盾、多吉云、七牛云就不一一介绍了。

六、CDN的使用

每个服务商 CDN 的配置界面都不大一样,不好写怎么配置,所以我大致说下 CDN 配置的思路。

1、首先保证域名A记录解析源站IP可以正常访问。

2、接入CDN主要有两种方式,第一种是 NS 解析,第二种是 CNAME 解析,大部分情况使用 CNAME 解析就行了。

3、选择 CNAME 解析后在选择的 CDN 服务商处添加网站,然后添加解析记录,添加解析记录之后会生成一个 CNAME 域名值。

4、复制这个 CNAME 域名值,进入你的域名解析服务商界面处,将解析记录解析成 CDN 生成的 CNAME 域名值。

配置成功之后就可以愉快的使用 CDN 了,当然这只是最基本的配置,还有很多需要注意的地方,例如缓存策略,缓存时间,免费套餐的访问带宽阈值,免费流量使用完之后的策略…都是需要注意的地方。

因为百度云加速小二已经配置好了,不想还原设置重新设置,所以下面我就按上面的步骤以奇安信 CDN 为例说下怎么配置 CDN。

1、首先保证域名解析源站能访问这就不用说了吧。

2、奇安信 CDN 官网注册账号实名认证后,在域名列表页面,输入你的域名,选择 CNAME 解析,点击添加域名。

添加cdn主域名

3、添加域名后点击添加子域名,添加记录,小二选择的是回源到 IP,所以添加两条A记录,如果是回源到域名,由别的域名提供IP,则添加 CNAME 记录。小二添加了 itwxe.com 和 www.itwxe.com 两条记录,配置结果如下图,最终会生成两条记录,这就是我们在域名解析处需要用到的两条记录。

添加cdn解析子域名

CDN解析记录配置

4、在解析出得到两条解析记录后,进入域名解析配置界面,小二的域名是在腾讯云购买的,解析服务商是 DNSPod,复制步骤三的两条记录添加到 DNSPod 中,同时需要注意的是需要一一对应,这样就就完成了最简单的 CDN 配置了。小二的示例如下图。

域名解析

等待 DNSPod 解析更改生效,腾讯云免费版DNS解析套餐更改记录生效时间是 10 分钟,此时可以再次使用 dig 命令,可以看到奇安信 CDN 已经生效了,不再是百度云加速。

…

等了10分钟,我也以为要成功了,毕竟小二可是老司机了,结果一访问网站,发现访问不了了!

以小二多年跳坑的经验,九成是CDN节点和源站SSL证书之间证书的问题,待我上传个 SSL 证书啥问题都解决了,当我以为故事到这里就结束了,结果…

奇安信不支持泛域名证书设置

不支持泛域名的话还是老老实实百度云加速吧,一个一个配置子域名可太累了…

当然配置 CDN 主要的流程就是上面的4个步骤,配置过程中有问题的客官欢迎留言评论。

都读到这里了,来个 点赞、评论、关注、收藏 吧!

本文转载自: 掘金

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

1…218219220…956

开发者博客

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