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

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


  • 首页

  • 归档

  • 搜索

Jeecg生成的代码不合口味?来改动一下

发表于 2021-11-18

前言

在使用Jeecg二开的过程中,体验到了代码生成器的便捷所在,非常的快速就能够基于数据库中的表生成出一套前后端都涵盖的CRUD代码来。但是Jeecg的模板并没有遵循RESTful风格规范,并且使用了Result.error来返回错误的情况而不是抛出异常来。如果我们能让生成出来的代码符合我们的口味,这也能让我们在随后的开发过程更加的舒适。

先看看官方文档

生成器官方文档:doc.jeecg.com/2043918
官方文档给出了Online表单中代码生成器模板的路径:jeecg-boot-module-system/jeecg.code-template-online(注意是online后缀的,无后缀的是已弃用的GUI的模板)

image.png

我们以一对多内嵌Table模板为示例进行演示,基于几个方面进行修改:

  1. 符合RESTful风格API
  2. 去掉接口的I前缀
  3. Mapper和Service接口去掉冗余的public修饰符
  4. ServiceImpl中使用@Resource进行Mapper注入,因为@Autowired对于Mapper在idea会爆红
  5. Controller的getById接口当未查询到数据时抛出异常而不是返回Result.error()
  6. 基于ResponseAdvice将返回值改为实体类型而不是Result<?>

首先进入jeecg-boot-module-system/jeecg.code-template-online如下图所选中的包

image.png

1. 符合RESTful风格API

打开controller模板${entityName}Controller.javai通过搜索功能分别进行修改

1
2
3
4
5
6
java复制代码@GetMapping(value = "/list")
@PostMapping(value = "/add")
@PutMapping(value = "/edit")
@DeleteMapping(value = "/delete")
@GetMapping(value = "/queryById")
@GetMapping(value = "/query${sub.entityName}ByMainId")

修改为

1
2
3
4
5
6
java复制代码@GetMapping("/list")
@PostMapping
@PutMapping
@DeleteMapping
@GetMapping
@GetMapping("/${sub.entityName}ByMainId")

打开vue.${entityName}.vuei搜索url关键词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vue复制代码url: {
list: '${urlPrefix}/list',
delete: '${urlPrefix}/delete',
deleteBatch: '${urlPrefix}/deleteBatch',
exportXlsUrl: '${urlPrefix}/exportXls',
importExcelUrl: '${urlPrefix}/importExcel',
},

修改为

url: {
list: '${urlPrefix}/list',
delete: '${urlPrefix}',
deleteBatch: '${urlPrefix}/batch',
exportXlsUrl: '${urlPrefix}/exportXls',
importExcelUrl: '${urlPrefix}/importExcel',
},

${entityName}Form.vuei

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dart复制代码        url: {
add: "/${entityPackage}/${entityName?uncap_first}/add",
edit: "/${entityPackage}/${entityName?uncap_first}/edit",
<#list subTables as sub><#rt/>
${sub.entityName?uncap_first}: {
list: '/${entityPackage}/${entityName?uncap_first}/query${sub.entityName}ByMainId'
},
</#list>
}
}

修改为:

url: {
add: "zu/${entityName?uncap_first}",
edit: "zu/${entityName?uncap_first}",
<#list subTables as sub><#rt/>
${sub.entityName?uncap_first}: {
list: '/zu/${entityName?uncap_first}/${sub.entityName}ByMainId'
},
</#list>
}

还剩下vue.subTables.[1-n]SubTable.vuei

1
2
3
4
5
6
7
8
9
css复制代码url: {
listByMainId: '${urlPrefix}/query${sub.entityName}ByMainId',
},

修改为

url: {
listByMainId: '${urlPrefix}/${sub.entityName}ByMainId',
},

于上我们就完成了Jeecg中RESTful风格的模板调整啦,未完待续。

本文转载自: 掘金

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

Hutool 工具不糊涂

发表于 2021-11-18

前言

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战 。还在对项目中的工具类和工具方法进行封装吗?让 Hutool 帮你,它是项目中 util 包的友好替代,覆盖了 Java 开发底层的方方面面,既是大型项目中解决小问题的利器,也是小项目中的效率担当,它能让你专注业务,极大地提升开发效率,可以最大程度地避免自行封装出现的小问题、小 Bug。所以,学会如何高效、准确地使用项目开发过程中所需的工具方法非常有必要,今天结合使用场景给大家分享一些实用的工具类方法,没讲清楚以及未涉及之处,望大家见谅,具体详情可参见 Hutool 官网 (hutool.cn) 。

引入

Hutool 包含对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行封装,放在不同的模块组件当中,当你需要对 Excel 进行操作时,你可以单独引入 hutool-poi 模块,当然分不清个模块之间功能,图省事,也可以通过引入 hutool-all 方式引入所有模块。

Maven

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

Gradle

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

jar

下载传送门

使用

下面给大伙分享一些经常使用且使用的工具方法,有用到过的欢迎留言评论哦 (^▽^)

Console 对象信息打印类

熟悉 JS 的掘友看到这两个方法一定不陌生,甚至是“旧交”了,其实 Hutool 中 的 Console 对象借鉴的就是 JS 中的语法糖。虽说是打印,和 System.out.println() / System.err.println() 还是有所不同的,最重要的是它支持 Slf4j 的字符串模版语法,会自动将对象(包括数组)转为字符串形式。

代码实例:

1
2
3
4
java复制代码
Console.log("this is array: {}", new String[]{"Java", "JavaScript", "Python"});

Console.error("I'm a red error message.");

这里的 {} 作为模版占位符,能够将逗号右边的变量值依次传入,从而以字符串的形式打印输出。

Convert 不同类型之间转换类

该类中封装了针对 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
java复制代码
// 转换为字符串
int number = 10;
Console.log("转换成字符串后:{}", Convert.toStr(number));

int[] arr = {1, 2, 3, 4, 5};
Console.log(Convert.toStr(arr, "Convert to string fail! This is default value."));

// 转换为指定类型的数组
String[] letters = {"12", "122", "1223", "1211"};
Console.log(Convert.toShortArray(letters));
Console.log(Convert.toStrArray(arr));

float[] floatArr = {1.2F, 1.3F, 2.0F, 3.14f};
Integer[] integerArr = Convert.toIntArray(floatArr);
Console.log(integerArr); // [1, 1, 2, 3]

// 转换为日期对象
String strDate1 = "2021-10-11 12:03:29";
// toDate
Console.log("The string of data convert to Date: {}", Convert.toDate(strDate1)); // 2021-10-11 12:03:29
// toLocalDateTime
Console.log("The string of data convert to LocalDateTime: {}", Convert.toLocalDateTime(strDate1)); // 2021-10-11T12:03:29

// 转换成集合
String[] langs = {"Java", "JavaScript", "Python", "C++", "GoLang", "TypeScript", "Kotlin"};

// 通过 Convert.convert(Class<T>, Object) 方法可以将任意类型转换为指定类型
ArrayList arrayList = Convert.convert(ArrayList.class, langs);
// 也可以这样转集合
List<String> langList = Convert.toList(String.class, langs);
Console.log("String array is converted to ArrayList: {}", arrayList);
Console.log("String array is converted to List: {}", langList);
// 转成指定类型的集合
Object[] objArr = {"a", "你", "好", "", 1};
// TypeReference 对象可以对嵌套泛型进行类型转换
List<String> strList = Convert.convert(new TypeReference<List<String>>() {
}, objArr);
Console.log("使用 TypeReference 对象可以对嵌套泛型进行类型转换:{}", strList);

字符串转为 16 进制(Hex)和 Unicode 串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
// 字符串转为 16 进制(Hex)
String desc = "大家好,我的名字叫 HUALEI !";
// 因为字符串牵涉到编码问题,因此必须传入编码对象
String hexStr = Convert.toHex(desc, CharsetUtil.CHARSET_UTF_8);
Console.log("将字符串转为 Hex 字符串:{}", hexStr); // e5a4a7e5aeb6e5a5bdefbc8ce68891e79a84e5908de5ad97e58fab204855414c454920efbc81

// Hex 字符串转换回去
String originStr = Convert.hexToStr(hexStr, CharsetUtil.CHARSET_UTF_8);
Console.log("将 Hex 字符串转换回原先字符串:{}", originStr);

/*
如果把各种文字编码形容为各地的方言,那么 Unicode 就是世界各国合作开发的一种语言。

字符串转为 Unicode(统一码 / 万国码 / 单一码),它是为了解决传统字符编码方案弊端(繁杂不统一,导致编码格式不一致引发的乱码问题)

Unicode 为每种语言中的每个字符设定了 统一 并且 唯一 的 二进制编码
以满足 跨语言、跨平台 进行 文本转换、处理 的需求
*/
// 将字符串转换成 Unicode
String unicodeStr = Convert.strToUnicode(desc);
Console.log("字符串转换成 Unicode 串:{}", unicodeStr); // \u5927\u5bb6\u597d\uff0c\u6211\u7684\u540d\u5b57\u53eb HUALEI \uff01

字符编码方式转换(编码与解码)

1
2
3
4
5
6
7
8
9
java复制代码
String a = "我不是乱码";
// 使用 UTF8 字符编码解码为 ISO_8859_1 编码方式的字符串
String mess = Convert.convertCharset(a, CharsetUtil.UTF_8, CharsetUtil.ISO_8859_1);
Console.log("ISO_8859_1 编码方式的中文乱码:{}", mess); // 转换后 mess 为乱码 => 我不是乱码

// 使用 ISO_8859_1 字符编码解码为原先的 UTF8 编码方式的字符串,将乱码转为正确的编码方式:
String raw = Convert.convertCharset(mess, CharsetUtil.ISO_8859_1, "UTF-8");
Console.log("将乱码转为正确的编码后:{}", raw); // 我不是乱码

中文大/小写数字、金额相关转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
// 金额转中文表达(最多保留到小数点后两位)
double money = 18.88D;
String chineseWord = Convert.digitToChinese(money);
Console.log("金额转中文表达:{}", chineseWord); // 壹拾捌元捌角捌分

// 金额转英文表达(最多保留到小数点后两位)
String englishWord = Convert.numberToWord(money);
Console.log("金额转英文表达:{}", englishWord); // EIGHTEEN AND CENTS EIGHTY EIGHT ONLY

double amount = 102389942.32D;
// 数字转中文大写(例:壹仟,最多保留到小数点后两位) isUseTraditional => true
String traditionalChinese = Convert.numberToChinese(amount, true);
// 数字转中文小写(例:一千) isUseTraditional => false
String nonTraditionalChinese = Convert.numberToChinese(amount, false);
Console.log("数字转中文大写,传统:{};非传统:{}", traditionalChinese, nonTraditionalChinese); // 传统:壹亿零贰佰叁拾捌万玖仟玖佰肆拾贰点叁贰;非传统:一亿零二百三十八万九千九百四十二点三二

// 中文大写壹仟叁佰转成纯数字
Console.log("中文大写壹仟叁佰转成数字:{}", Convert.chineseToNumber("壹仟叁佰")); // 1300

// 数字简化 1000 => 1k; 10000 => 1w(最多保留到小数点后两位)
String simple = Convert.numberToSimple(amount);
Console.log("{} 简化为:{}", amount, simple); // 10238.99w

convertTime(目标值,目标值时间单位,转换后的时间单位) 方法主要用于转换时长单位,比如一个很大的毫秒,我想获得这个毫秒数换算成多少天:

1
2
3
4
5
java复制代码
// 毫秒数
long ms = 100000000L;
long msToDays = Convert.convertTime(ms, TimeUnit.MILLISECONDS, TimeUnit.DAYS);
Console.log("{} 毫秒约等于 {} 天", ms, msToDays); // 100000000 毫秒约等于 1 天

原始类型和包装类型之间的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
// Integer 包装类
Class<Integer> wrapClass = Integer.class;

// Integer 去除包装类 => int
Class<?> unWraped = Convert.unWrap(wrapClass); // int
Console.log("Integer 包装类去包装化:{}", unWraped);

// 原始类
Class<?> primitiveClass = long.class;

// 将原始类型转换成包装类
Class<?> wraped = Convert.wrap(primitiveClass); // class java.lang.Long
Console.log("long 原始类包装化:{}", wraped);

DateUtil DateTime 日期时间工具类

对于 java.util.Date 对象是不是还停留在被 Thu Nov dd HH:mm:ss CST yyyy 支配的恐惧中?数字英文单词缩写的混杂让人看起来非常不舒服,如果你想将其转换成 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss 时间格式字符串,你是不是得先 new 一个 SimpleDateFormat,通过 pattern 参数进行初始化格式化器,时间格式不熟练的可能还要百度一下,甚是麻烦。

为了便捷,Hutool 工具使用了一个 DateTime 类来替代之,继承自 Date ,重写了 toString() 方法,直接放回 yyyy-MM-dd HH:mm:ss 形式的字符串,方便在输出时的调用(例如日志记录等),提供了众多便捷的方法对日期对象操作。

代码实例:

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
java复制代码
// 计时器,计算被其包裹的代码片段执行时间
final TimeInterval timeInterval = DateUtil.timer();

// 根据 birthday 计算现在的年龄
String dateStr = "2000年05月26日";
Date birthday = Convert.toDate(dateStr);
// 可以通过 DatePattern 指定 dateStr 日期时间格式进行解析,如果没有第二个参数,则会自动寻找合适的格式(支持多种格式)进行解析
// DateTime dateTime = new DateTime(dateStr, DatePattern.CHINESE_DATE_PATTERN);
int age = DateUtil.ageOfNow(birthday); // 或 DateUtil.ageOfNow(dateTime);
Console.log("{} 出生的小伙子,如今已经 {} 岁了!", dateStr, age);

// 获取当前时间,格式:yyyy-MM-dd HH:mm:ss => DateTime 类型
Date currentDate = DateUtil.date(); // 2021-11-02 08:57:54
// 等价于 DateTime currentDateTime = new DateTime();
// 不同于 new Date() => Tue Nov 02 08:57:54 CST 2021
Console.log("DateUtil.date() 得到当前时间(yyyy-MM-dd HH:mm:ss):", currentDate);

// 将特殊的日期字符串转换为 DateTime 对象
String strDate2 = "2021年11月02日 12时03分29秒";
// 根据格式化模版将字符串日期转换为 Date 对象
Console.log("Use DateUtil.parse() method convert to Date: {}", DateUtil.parse(strDate2, "yyyy年MM月dd日 HH时mm分ss秒")); // 2021-11-02 12:03:29
// 等价于 new DateTime(strDate2);

// 方式一:当前毫秒数转换为 DateTime 时间,格式:yyyy-MM-dd HH:mm:ss
Date msToDateTime1 = DateUtil.date(Calendar.getInstance()); // 2021-11-02 08:57:54
Console.log("当前毫秒数转换为 DateTime 时间,方式一:", msToDateTime1);

// 方式二:当前毫秒数转换成 DateTime 时间,格式:yyyy-MM-dd HH:mm:ss
Date msToDateTime2 = DateUtil.date(System.currentTimeMillis()); // 2021-11-02 08:57:54
Console.log("当前毫秒数转换为 DateTime 时间,方式二:", msToDateTime2);

// 当前日期字符串,格式:yyyy-MM-dd
String today= DateUtil.today(); // 2021-11-02
Console.log("当前日期字符串,格式:yyyy-MM-dd:{}", today);

// 将 Date 对象格式化为日期字符串,格式:yyyy-MM-dd
String formatDateStr = DateUtil.format(new Date(), "yyyy-MM-dd");
Console.log("Use DateUtil.format() method convert to Date: {}", formatDateStr);

// 获取日期时间(DateTime)的 Time 字符串,格式:HH:mm:ss
String formatTimeStr = DateUtil.formatTime(new DateTime());
Console.log("Use DateUtil.formatTime() method only covert Date to HH:mm:ss:{}", formatTimeStr);

// 星座(zodiac sign)
String zodiac = DateUtil.getZodiac(DateUtil.month(birthday), ((int) DateUtil.betweenDay(DateUtil.beginOfMonth(birthday), birthday, false)));
// 属相(chinese zodiac)
String chineseZodiac = DateUtil.getChineseZodiac(DateUtil.year(birthday));
Console.log("{} 生日的人,星座是:{},属 {} 的。", birthday, zodiac, chineseZodiac);

// 判断今年是不是平闰年
boolean isLeapYear = DateUtil.isLeapYear(DateUtil.year(new Date()));
Console.log("{}年是闰年吗?{}", DateUtil.year(new Date()), isLeapYear);

// 得到一个美化过后的花费时间,即加上中文单位
String spendTimePretty = timeInterval.intervalPretty();
// 运行时间转换为秒为单位
long sec = timeInterval.intervalSecond();

// 返回花费时间,并重置开始时间
long spendTimeAndRestart = timeInterval.intervalRestart();

Console.log("测试运行时间(无单位不美化):", spendTimeAndRestart);
Console.log("测试运行时间(有单位带美化):", spendTimePretty);
Console.log("测试运行时间,转换为秒:{}", sec);

获取指定天/周/月/季度/年的开始/结束时间,返回 DateTime,格式为:yyyy-MM-dd HH:mm:ss

1
2
3
4
5
6
7
8
9
java复制代码
DateTime beginOfDayTime = DateUtil.beginOfDay(new Date());
Console.log("今天开始时间:{}", beginOfDayTime); // 2021-11-03 00:00:00

DateTime beginOfYearTime = DateUtil.beginOfYear(new Date());
Console.log("今年开始时间:{}", beginOfYearTime); // 2021-01-01 00:00:00

DateTime endOfYearTime = DateUtil.endOfYear(new Date());
Console.log("今年结束时间:{}", endOfYearTime); // 2021-12-31 23:59:59

日期时间偏移:即对某个日期增减分、小时、天等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
// 将当前时间增加半个小时
DateTime halfHourLater = DateUtil.offset(currentDate, DateField.MINUTE, 30);
// 等同于:DateUtil.offsetHour(currentDate, 30);
Console.log("当前时间是 {},半个小时后是:{}", currentDate, halfHourLater);
// 五天后的现在时间
DateTime fiveDaysFromNow = DateUtil.offsetDay(currentDate, 5);

Console.log("当前时间是 {},五天后是:{}", currentDate, fiveDaysFromNow);
// 针对当前时间,提供了更为简化的偏移方法
Console.log("昨天的现在是:{}", DateUtil.yesterday());
Console.log("明天的现在是:{}", DateUtil.tomorrow());
Console.log("上周天的现在是:{}", DateUtil.lastWeek());
Console.log("下周的现在是:{}", DateUtil.nextWeek());
Console.log("上个月的现在是:{}", DateUtil.lastMonth());
Console.log("下个月的现在是:{}", DateUtil.nextMonth());

计算两个时间的间隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
// 当前时间
DateTime currentDateTime = new DateTime();

String dateTimeStrStart = "1949/10/01";
Date startTime = Convert.toDate(dateTimeStrStart);
long betweenDays = DateUtil.between(startTime, new DateTime(), DateUnit.DAY);
Console.log("距离新中国成立,已经过了 {} 天", betweenDays);

// 下班时间(当天 17:00:00 )
Date getOffTime = DateUtil.offsetHour(DateUtil.beginOfDay(currentDateTime), 17);
Console.log("下班时间是:{}", getOffTime);
long distance = DateUtil.between(getOffTime, currentDateTime, DateUnit.SECOND, true);

// 间隔时间设置为秒
Console.log("距离下班还剩 {} 秒", distance);
// 格式化间隔时间
Console.log("距离下班还剩 {} 精确到秒", DateUtil.formatBetween(getOffTime, currentDateTime, BetweenFormatter.Level.SECOND));

ObjectUtil 对象信息打印类

针对 Object 通用的工具类方法,不区分是 String 还是 Object 、Array 还是 Collection。

代码实例:

判断 null / blank string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码final String blankStr = "   ";
final String nullStr = null;
final String dateStr = null;

// isEmpty/isNull 只有为 null 才返回 true
System.out.println(ObjectUtil.isEmpty(blankStr)); // false
System.out.println(ObjectUtil.isEmpty(nullStr)); // true
System.out.println(ObjectUtil.isNull(nullStr)); // true
System.out.println(ObjectUtil.isNull(blankStr)); // false

// null/空格字符串都是 blank
System.out.println(ObjectUtil.defaultIfBlank(blankStr, "目标字符串为空,我是默认")); // 走 defaultValue
System.out.println(ObjectUtil.defaultIfBlank(nullStr, "目标字符串为空,我是默认")); // 走 defaultValue

// 此处判断如果dateStr为null,则调用`Instant.now()`,不为null则执行`DateUtil.parse`
System.out.println(ObjectUtil.defaultIfNull(dateStr, () -> DateUtil.parse(dateStr, DatePattern.NORM_DATETIME_PATTERN).toInstant(), Instant.now()));

System.out.println(ObjectUtil.defaultIfNull(nullStr, "目标字符串为 null ,我是默认")); // 走 defaultValue
System.out.println(ObjectUtil.defaultIfNull(blankStr, "目标字符串为 null ,我是默认")); // blankStr

注意: ObjectUtil.isNull() 方法不能判断对象中字段为空的情况,如果需要检查Bean对象中字段是否全空,需使用 BeanUtil.isEmpty() 方法进行判定。

判断两个对象是否相等,等价于 Objects.equals() 方法

1
2
3
4
5
6
java复制代码
ObjectUtil.equals(nullStr, dateStr); // true
ObjectUtil.equals("HUALIE", "HUALEI"); // true

// 等价于
Objects.equals(nullStr, dateStr); // true

计算对象长度

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
int[] array = new int[]{1,2,3,4,5};
// 计算数组长度
int length = ObjectUtil.length(array); // 5

Map<String, String> map = new HashMap<>();
map.put("a", "a1");
map.put("b", "b1");
map.put("c", "c1");

// 计算 Map 大小
length = ObjectUtil.length(map); // 3

如果是字符串调用其 length() 方法,集合类调用其 size() 方法,数组调用其length 属性,其他可遍历对象遍历计算长度。

StrUtil 处理字符串工具

image.png

↑ 和这个类类似,但是相较于 StringUtils 这个类写起来更短,更能偷懒 [狗头],类中常用的方法有 isBlank()、isNotBlank()、isEmpty()、isNotEmpty() 主要用于对字符串的判空,这里就不赘述了。

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
// 去除前缀
String[] tableNames = {"tb_user", "t_mall", "province", "t_capital", "tb_task"};
for (int i = 0; i < tableNames.length; i++) {
if (StrUtil.containsAnyIgnoreCase(tableNames[i], "tb_", "t_")) {
// 去除前缀忽略大小写
tableNames[i] = StrUtil.removePrefixIgnoreCase(tableNames[i], "tb_");
// 去除前缀忽略大小写
tableNames[i] = StrUtil.removePrefixIgnoreCase(tableNames[i], "t_");
}
}
Console.log("移除表名前缀后:{}", (Object) tableNames); // [user, mall, province, capital, task]

// 去除后缀
String fileName = StrUtil.removeSuffix("HUALEI.png", ".png");
Console.log("去除文件.扩展名后:{}", fileName); // HUALEI

// 字符创常量,包括点、空字符串、换行符,还有一些 HTML 中的转义字符
System.out.println(StrUtil.AT.concat("HUALEI").concat(StrUtil.DOT).concat(StrUtil.LF)); // @HUALEI.

// 使用字符串模板代替字符串拼接
String template = "{}爱{},我爱你,蜜雪冰城{}";
String str = StrUtil.format(template, "你", "我", "甜蜜蜜"); // 你爱我,我爱你,蜜雪冰城甜蜜蜜

CollUtil | CollStreamUtil 集合处理工具类

这两个类主要封装的是对数组、列表等集合类的操作方法,集合在开发中占很大一部分,必须重点掌握,以提高我们的开发效率。

代码实例:

模拟数据库排序分页,返回一个 PageResult

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复制代码
List<Fruit> fruits = fruitMapper.selectList(Wrappers.emptyWrapper());

// 总记录数
long totalCount = fruits.size();
// 当前页
int currentPage = 1;
// 每页记录数
int pageSize = 10;

// 当前页索引,从 0 开始,表示第一页
int pageNo = currentPage - 1;

// 默认分页列表
List<Fruit> defaultPageList = CollUtil.page(pageNo, pageSize, fruits);

// 按照记录更新时间进行排序分页列表
List<Fruit> orderPageList = CollUtil.sortPageAll(pageNo, pageSize, Comparator.comparing(Fruit::getUpdateTime), fruits);

// 重新用分页参数及记录列表组装 PageResult 分页结果对象
PageResult pageResult = new PageResult(orderPageList, totalCount, pageSize, currentPage));

System.out.println("当前页数:" + pageResult.getCurrPage());
System.out.println("每页记录数:" + pageResult.getPageSize());
System.out.println("总记录数:" + pageResult.getTotalCount());
System.out.println("总页数:" + pageResult.getTotalPage());
// 当前页记录列表
pageResult.getList().forEach(System.out::println);

获取表头别名映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
// 字段名列表
List<String> headerNameList = new ArrayList<>();

// 使用反射工具获取全部表头名
for (Field field : ReflectUtil.getFields(ProvinceExcelVO.class)) {
if (!StrUtil.equals(field.getName(), "serialVersionUID")) {
headerNameList.add(field.getName());
}
}
// 表头别名列表
List<String> aliasList = CollUtil.newArrayList("省份名称", "简称", "面积(km²)", "人口数量(万)", "著名景点", "邮政编码", "省会城市", "别称", "气候类型", "车牌号");

// 一一对应,形成表头别名映射
Map<String, String> headerAliasMap = CollUtil.zip(fieldNameList, aliasList);

建立省名和省份对象之间的 Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
// 省份表中所有省份信息
List<Province> provinces = provinceMapper.selectList(Wrappers.emptyWrapper());

// 使用 CollStreamUtil 集合流工具,一行代码解决
Map<String, Province> provinceMap = CollStreamUtil.toIdentityMap(provinces, Province::getProvince);

// 等价于
Map<String, List<Province>> provinceMap =
provinces.stream()
.collect(Collectors.toMap(Province::getProvince, Arrays::asList));

// 又等价于,结合 Convert 方法二:
Map<String, List<Province>> provinceMap =
provinces.stream()
.collect(Collectors.toMap(Province::getProvince, p -> Convert.toList(Province.class, p)));

// 获取江西省省份信息
Console.log("江西省省份信息为:{}", provinceMap.get("江西省"));

将所有省份按照主键 pid 进行分组

1
2
3
4
5
6
java复制代码
Map<Integer, List<Province>> groupByKey = CollStreamUtil.groupByKey(CollUtil.unionAll(provinces, provinces), Province::getPid);

// 等价于
Map<Integer, List<Province>> groupByKey = CollUtil.unionAll(provinces, provinces).stream()
.collect(Collectors.groupingBy(Province::getPid, Collectors.toList()));

提取省份信息列表中省份名

1
2
3
4
5
6
7
java复制代码        
List<String> provinceNameList = CollStreamUtil.toList(provinces, Province::getProvince);

// 等价于
List<String> provinceNameList = provinces.stream()
.map(Province::getProvince)
.collect(Collectors.toList());

两个 map 根据 key 进行合并(merge)操作,最终结果期望为:(key: pid, value: province(省份名) + “-“ + abbr(省份简称))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
// (key: pid, value: province)
Map<Integer, String> pidToProvince = CollStreamUtil.toMap(provinces, Province::getPid, Province::getProvince);

// 等价于
Map<Integer, String> pidToProvince =
provinces.stream()
.collect(Collectors.toMap(Province::getPid, Province::getProvince));

// (key: pid, value: abbr)
Map<Integer, String> pidToAbbr = CollStreamUtil.toMap(provinces, Province::getPid, Province::getAbbr);

// 等价于
Map<Integer, String> pidToProvince =
provinces.stream()
.collect(Collectors.toMap(Province::getPid, Province::getAbbr));

// 根据 key 合并 Map,key 相同则走 merge 规则
Map<Integer, String> mergeResult = CollStreamUtil.merge(pidToProvince, pidToAbbr, (province, abbr) -> province + "-" + abbr);

RandomUtil 随机工具

RandomUtil 工具类主要是对 Random 对象的封装,用于产生随机数/列表,虽然是伪随机数,但是对于大多数使用场景还是够用的。

代码实例:

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复制代码
// 生成指定范围内的随机整数
for (int i = 0; i < 10; i++) {
System.out.println(RandomUtil.randomInt(0, 100));
}

List<String> randomList = new ArrayList<>();
// 生成随机字符串(只包含数字和字符)
for (int i = 0; i < 10; i++) {
randomList.add(RandomUtil.randomString(5));
}

// 从指定列表中随机取 5 个数
List<Integer> eleList = RandomUtil.randomEleList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 5);

// 随机从字符串列表中取出 5 个,不重复(取出的个数不能超过列表的大小)
Set<String> eleSet = RandomUtil.randomEleSet(randomList, 5);

// 权重随机生成器,传入带权重的对象,然后根据权重随机获取对象
WeightRandom weightRandom = RandomUtil.weightRandom(new WeightRandom.WeightObj[]{new WeightRandom.WeightObj("HUA", 60), new WeightRandom.WeightObj("L", 10), new WeightRandom.WeightObj("EI", 30)});

// 随机取 10 次,"HUA" 字符串出现的次数是最多的
for (int i = 0; i < 10; i++) {
System.out.println(weightRandom.next());
}

ExcelUtil Excel 操作工具类

该类是对 Apache 的 POI 库的二次封装,便捷了对 MS Office 文档操作,使用该类时,需要用户执行导入 poi-ooxml 依赖,这个包会自动关联引入 poi 包,可以很好的支持 Office2007+ 的文档格式。

注意

开始使用前,需导入依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>

未引入使用其中的工具方法,会报错:

1
console复制代码You need to add dependency of 'poi-ooxml' to your project, and version >= 4.1.2

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
java复制代码// 写入 Excel 中的数据源
List<Province> provinces = provinceMapper.selectList(Wrappers.emptyWrapper());

// 写出文件路径
String inputPath = "D:/桌面/导出省份信息.xlsx";
// 通过工具类创建一个 Writer 写入对象
ExcelWriter excelWriter = ExcelUtil.getWriter(inputPath, "省份信息表");

// 自定义表头名(方法一)
excelWriter.addHeaderAlias("pid", "省份ID");
excelWriter.addHeaderAlias("province", "省份");
excelWriter.addHeaderAlias("abbr", "简称");
excelWriter.addHeaderAlias("area", "面积(km²)");
excelWriter.addHeaderAlias("population", "人口数量(万)");
excelWriter.addHeaderAlias("attraction", "著名景点");
excelWriter.addHeaderAlias("capital", "省会");

// 自定义表头名(方法二)
Map<String, String> headerAlias = new LinkedHashMap<>(7);

for (Field field : ReflectUtil.getFields(Province.class)) {
String name = field.getName();
String alias;
switch (name) {
case "pid":
alias = "省份ID";
break;
case "province":
alias = "省份";
break;
case "abbr":
alias = "简称";
break;
case "area":
alias = "面积(km²)";
break;
case "population":
alias = "人口数量(万)";
break;
case "attraction":
alias = "著名景点";
break;
case "capital":
alias = "省会";
break;
default:
alias = null;
}
if (ObjectUtil.isNotEmpty(alias)) {
headerAlias.put(field.getName(), alias);
}
}

// 使用无序的 HashMap 入参表头也跟着无序,需使用 LinkedHashMap ,保证有序
excelWriter.setHeaderAlias(headerAlias);

// 写出数据源到 Excel 当中,使用默认样式,强制输出标题
excelWriter.write(provinces, true);

// 关闭 writer ,释放内存
excelWriter.close();

结尾

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

参考

Overview (hutool-码云(gitee.com))

本文转载自: 掘金

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

Ribbon 的核心组件

发表于 2021-11-18

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

你好,我是悟空呀。

Ribbon 是 Spring Cloud 中负载均衡组件。

Ribbon 主要有六大功能组件:LoadBalancer,ServerList、Rule、Ping、ServerListFilter、ServerListUpdater。

Ribbon 核心组件

3.1 负载均衡器 LoadBalancer

用于管理负载均衡的组件。初始化的时候通过加载 YMAL 配置文件创建出来的。

3.2 服务列表 ServerList

ServerList 主要用来获取所有服务的地址信息,并存到本地。

根据获取服务信息的方式不同,又分为静态存储和动态存储。

静态存储:从配置文件中获取服务节点列表并存储到本地。

动态存储:从注册中心获取服务节点列表并存储到本地

3.3 服务列表过滤 ServerListFilter

将获取到的服务列表按照过滤规则过滤。

  • 通过 Eureka 的分区规则对服务实例进行过滤。
  • 比较服务实例的通信失败数和并发连接数来剔除不够健康的实例。
  • 根据所属区域过滤出同区域的服务实例。

3.4 服务列表更新 ServerListUpdater

服务列表更新就是 Ribbon 会从注册中心获取最新的注册表信息。是由这个接口 ServerListUpdater 定义的更新操作。而它有两个实现类,也就是有两种更新方式:

  • 通过定时任务进行更新。由这个实现类 PollingServerListUpdater 做到的。
  • 利用 Eureka 的事件监听器来更新。由这个实现类 EurekaNotificationServerListUpdater 做到的。

3.5 心跳检测 Ping

IPing 接口类用来检测哪些服务可用。如果不可用了,就剔除这些服务。

实现类主要有这几个:PingUrl、PingConstant、NoOpPing、DummyPing、NIWSDiscoveryPing。

心跳检测策略对象 IPingStrategy,默认实现是轮询检测。

3.6 负载均衡策略 Rule

Ribbon 的负载均衡策略和之前讲过的负载均衡策略有部分相同,先来个全面的图,看下 Ribbon 有哪几种负载均衡策略。

再来看下 Ribbon 源码中关于均衡策略的 UML 类图。

负载均衡策略

由图可以看到,主要由以下几种均衡策略:

  • 线性轮询均衡 (RoundRobinRule):轮流依次请求不同的服务器。优点是无需记录当前所有连接的状态,无状态调度。
  • 可用服务过滤负载均衡(AvailabilityFilteringRule):过滤多次访问故障而处于断路器状态的服务,还有过滤并发连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问。默认情况下,如果最近三次连接均失败,则认为该服务实例断路。然后保持 30s 后进入回路关闭状态,如果此时仍然连接失败,那么等待进入关闭状态的时间会随着失败次数的增加呈指数级增长。
  • 加权响应时间负载均衡(WeightedResponseTimeRule):为每个服务按响应时长自动分配权重,响应时间越长,权重越低,被选中的概率越低。
  • 区域感知负载均衡(ZoneAvoidanceRule):更倾向于选择发出调用的服务所在的托管区域内的服务,降低延迟,节省成本。Spring Cloud Ribbon 中默认的策略。
  • 重试负载均衡(RetryRule):通过轮询均衡策略选择一个服务器,如果请求失败或响应超时,可以选择重试当前服务节点,也可以选择其他节点。
  • 高可用(Best Available):忽略请求失败的服务器,尽量找并发比较低的服务器。注意:这种会给服务器集群带来成倍的压力。
  • 随机负载均衡(RandomRule):随机选择服务器。适合并发比较大的场景。

本文转载自: 掘金

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

Spring-Aop和声明式事务 AOP介绍

发表于 2021-11-18

AOP介绍

1
2
markdown复制代码aop:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者**运行期动态代理**(Spring)实现程序功能的统一维护的技术。
AOP是OOP(面向对象编程)的技术延续,是软件开发中的一个热点,也是Spring中的一个重要内容。利用AOP可以实现对业务逻辑各个部分之间的隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时提高了开发效率。
  • aop的作用:在不修改源码的情况下,进行功能增强,通过动态代理实现的

  • 1
    复制代码 优势:减少重复代码,提高开发效率,方便维护
  • aop的底层实现:底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标的方法从而完成功能的增强

1
复制代码   1.常见的动态代理技术有:
  1. jdk的动态代理:基于接口实现的
  2. cglib的动态代理:基于子类实现的
    Spring的aop采用了哪种代理方式
  3. 如果目标对象有接口,就采用JDK动态代理技术
  4. 如果目标对象没有接口就采用cglib技术

AOP相关概念

1
2
3
4
markdown复制代码连接点:能够被增强的方法都是连接点
切入点:已经增强的连接点叫切入点
通知/增强:对目标对象增强的代码
切面:是切入点和切面的结合

AOP开发前要明确的事项

我们需要做的事情

1
2
3
复制代码编写核心业务(目标类的相关方法)
编写通知类,通知类有通知方法(Advice增强功能方法)
在配置文件中配置织入关系,完成完整的代码逻辑运行

Spring的AOP做的事情

1
2
markdown复制代码生成动态代理的过程(把通知织入到切入点的过程),是由spring来实现的
Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知的类别,在代理对象的对应位置,将通知对应的功能织入完成完整代码的逻辑运行

基于xml的AOP

1
复制代码入门实现:
1.导入aop的依赖

image.png

2.创建目标类和通知类
1
2
3
4
5
csharp复制代码    public interface UserService {
void add();

void update();
}
1
2
3
4
5
6
7
8
9
csharp复制代码    public class UserServiceImpl implements UserService {
public void add() {
System.out.println("调用了UserServiceImpl的add方法~!");
//int a = 1 / 0 ;
}

public void update() {
System.out.println("调用了UserServiceImpl的update方法~!");
}

通知类(增强类):

1
2
3
4
5
csharp复制代码    public class MyAdvice {

public void print(){
System.out.println("打印日志~!");
}
3.写配置文件

image.png

image.png

通知类型的介绍

1
2
3
4
5
xml复制代码    前置通知      <aop:before>          通知方法在切入点方法之前执行
后置通知 <aop:after-rurining> 在切入点方法执行后,执行通知方法
异常通知 <aop:after-throwing> 在切入点方法抛出异常时,执行通知方法
最终通知 <aop:after> 无论切入点是否有异常,最终都会执行该方法
环绕通知 <aop:around> 通知方法在切入点之前还有之后都会执行

切面表达式的抽取,如果我们对一个方法在不同位置进行增强时,切面表达式重复多余,所以我们可以进行抽取

image.png

基于注解的AOP

第一步:开启AOP自动代理和IOC扫描包

1
2
ini复制代码<context:component-scan base-package="com.albb"/>
<aop:aspectj-autoproxy/>

第二步:在增强类和目标类头上打上注解让spring为其创建对象

第三步:在增强类头上打上@Aspect注解,告诉spring我是增强类,是用来做增强的

第四步:在通知方法上打上@After(切面表达式)…注解告诉spring要增强在目标方法的哪个位置,要增强哪个方法

1
2
3
4
5
6
7
8
less复制代码@Component
@Aspect
public class MyAdvice {

@Before("execution(* com.itheima..*.*(..))")
public void print(){
System.out.println("打印日志~");
}

纯注解的方式

1
2
3
4
5
less复制代码纯注解是依赖于核心配置类的,我们创建一个核心配置类
在他头上打上
@configuration 表示这是一个核心配置类
@ComponentScan("com.albb“)表示开启ioc扫描包
@EnableAspectJAutoProxy 表示开启aop自动代理

基于XML的声明事务控制

1
复制代码1.定义事务的管理员

image.png

1
markdown复制代码2.定义事务规则

image.png
3.定义切面

image.png

注解的方式声明事务控制

1
2
复制代码1.定义事务管理员
2.开启事务注解
1
ini复制代码<tx:annotation-driven transaction-manager="tm"/>
1
css复制代码3.在要开启事务的类或者方法上添加@Transactional

纯注解的方式声明事务控制

1
2
3
4
java复制代码1.定义配置类
添加@EnableTransactionManager,表示开启事务注解
2.使用@Bean在配置类中创建事务管理员
3.给要使用事务的类或者方法添加@Transactional注解

本文转载自: 掘金

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

单线程的Redis为何如此之快? Redis其他常见知识点简

发表于 2021-11-18

Redis是一款使用C语言编写、可基于内存也可以持久化的日志型、Key-Value型开源数据库。它可以用作:数据库、缓存和消息中间件,也是目前最受欢迎的一款缓存中间件,总所周知,redis的核心操作是单线程实现的,那么为什么他还能有如此高的效率?本文将从数据结构,进程线程模型等方面进行简要叙述,顺便提一下redis的常见问题与算法

简单但是高效的数据结构

redis支持的5种基本数据类型

Redis效率如此高的原因之一是他专门设计了一套KV数据结构,常用的数据结构主要有线面五种:

数据类型 定义 使用场景
字符串(String) Redis的基本数据类型,一个Key对应一个Value 缓存、计数器、分布式锁等
哈希(Hash) Hash是一个string类型的key和value的映射表,特别适合用于存储对象 用户信息、Hash 表等
列表(List) List是简单的字符串列表,按照插入顺序排序。可以添加一个元素导列表的头部(左边)或者尾部(右边)。相当于链表 链表、队列、微博关注人时间轴列表等
集合(Set) Set是string类型的无序集合捕鱼必须重复,通过哈希表实现,添加,删除,查找的复杂度都是O(1) 去重、赞、踩、共同好友等
有序集合(Zset) zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。通过分数来为集合中的成员进行从小到大的排序 访问量排行榜、点击量排行榜等

以及,范围查询,Bitmaps,Hyperloglogs 和地理空间(Geospatial)索引半径查询

Redis为什么快?

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,官方提供的数据是可以达到 100000+ 的QPS(每秒内查询次数)

原因分析:

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的,如SDS,跳跃表等等
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  4. 使用多路I/O复用模型,非阻塞IO;
  5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis其他常见知识点简单总结

Redis与Memcached区别

  1. Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
  2. 内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
  3. 性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。
  4. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集群模式的,比memcached来说要更好。

缓存雪崩

redis缓存的key在同一时间大量的失效,导致大量的请求同事打到数据库,造成数据库压力过大
解决方案:

  • 合理地设置缓存的过期时间,过期时间加上随机值
  • 热点数据永不过期,定时刷新缓存数据
  • 互斥锁,拿到所再去访问数据库,性能有影响

缓存穿透

大量请求Redis并不存在的数据,导致请求大量的打到数据库,可能是攻击页可能是非法参数
解决方案:

  • 互斥锁,拿到所再去访问数据库,性能有影响
  • 返回null,异步更新
  • 合法参数校验, 布隆过滤器
  • 数据库查询为空的对象也放入缓存,但是过期时间设置的比较短

缓存击穿

热点key过期,导致大量请求直接达到数据库,造成数据库压力剧增
解决方案:

  • 互斥锁,分布式锁,只有一个线程能够抢到这个锁,也就只有一个线程能够进入到数据库,然后将数据放到缓存,其他线程就能从缓存拿到这个数据
  • 永不过期,定时刷新缓存

持久化

bgsave
手动,太麻烦

rdb配置文件配置持久化策略

省心
还是可能会会丢失数据

AOF

配置appendonly yes
有点:实时记录命令
文件大,时间长文件会很大

可以和rdb结合使用

主从同步

主从服用,读写分离

发布订阅模式

哨兵模式

主服务器挂掉之后,重新选出一个主服务器进行读写,主服务器恢复之后还是能作为集群的节点

key的过期淘汰机制

虽然Redis可以对缓存的key设置过期时间,但是并不是过期时间到了缓存的数据就一定会被淘汰

定期删除

默认每秒钟扫描10次,也就是每100ms扫描一次,过期扫描不会扫描全部数据(扫描全部性能小消耗太大),而是采用一种很简单的贪心策略:

  • 从过期字典中随机选择20个key
  • 删除这些key中的过期key
  • 如果删除的元素超过1/4就继续重复步骤1和2

惰性删除

查询的时候会看key是否过期,过期的话删除数据,不返回任何内容
弥补了定期删除的不足,可能有很多已经过期的key在定期删除的时候并没有被成功地删掉
定期删除是集资红处理,惰性删除则是零散处理

内存淘汰策略

不管是定期删除还是惰性删除,都不是一个完全精准的删除,还是会有key没有被删除的情况存在,于是就需要内存淘汰策略

  1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  3. volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的
  4. allkeys-random:加入键的时候如果过限,从所有key随机删除
  5. volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  8. allkeys-lfu:从所有键中驱逐使用频率最少的键

【参考】

【1】《Redis设计与实现》

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

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

Go语言,gRPC 的使用了解--下

发表于 2021-11-18

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

书接上文,我们继续实现剩余的两种方式–客户端流式 RPC、双向流式 RPC。

Client-side streaming RPC:客户端流式 RPC、

客户端流式 RPC,单向流,客户端通过流式发起多次 RPC 请求到服务端,服务端发起一次响应给客户端

Proto :

1
2
3
4
5
6
7
8
9
10
11
go复制代码syntax = "proto3";

package proto;

message String {
string value = 1;
}

service HelloService {
rpc Hello (stream String) returns (String){};
}

server:

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
go复制代码package main

import (
"google.golang.org/grpc"
"io"
"log"
"net"
pb "rpc/proto" // 设置引用别名
)

// HelloServiceImpl 定义我们的服务
type HelloServiceImpl struct{}

//实现Hello方法
func (p *HelloServiceImpl) Hello(stream pb.HelloService_HelloServer) error {
for {
resp, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&pb.String{Value:"say.hello"})
}
if err != nil {
return err
}

log.Printf("resp: %v", resp)
}

return nil
}

func main() {
// 新建gRPC服务器实例
grpcServer := grpc.NewServer()
// 在gRPC服务器注册我们的服务
pb.RegisterHelloServiceServer(grpcServer, new(HelloServiceImpl))

lis, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal(err)
}
log.Println(" net.Listing...")
//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
err = grpcServer.Serve(lis)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}

如上,我们对每一个 Recv 都进行了处理,当发现 io.EOF (流关闭) 后,需要通过 stream.SendAndClose 方法将最终的响应结果发送给客户端,同时关闭正在另外一侧等待的 Recv。

client:

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
go复制代码package main

import (
"context"
"google.golang.org/grpc"
"log"
pb "rpc/proto" // 设置引用别名
)

// SayHello 调用服务端的 Hello 方法
func SayHello(client pb.HelloServiceClient, r *pb.String) error {
stream, _ := client.Hello(context.Background())
for n := 0; n < 6; n++ {
_ = stream.Send(r)
}
resp, _ := stream.CloseAndRecv()

log.Printf("resp err: %v", resp)
return nil
}

func main() {
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
log.Fatal("dialing err:", err)
}
defer conn.Close()

// 建立gRPC连接
client := pb.NewHelloServiceClient(conn)

// 创建发送结构体
req := pb.String{
Value: "stream server grpc ",
}
SayHello(client, &req)
}

在 Server 端的 stream.CloseAndRecv,与 Client 端 stream.SendAndClose 是配套使用的方法。

开启服务器端,开启客户端。执行结果如下:

1
2
3
4
5
6
7
8
go复制代码$ go run server.go
2021/11/17 13:26:34 net.Listing...
2021/11/17 13:26:44 resp: value:"stream server grpc "
2021/11/17 13:26:44 resp: value:"stream server grpc "
2021/11/17 13:26:44 resp: value:"stream server grpc "
2021/11/17 13:26:44 resp: value:"stream server grpc "
2021/11/17 13:26:44 resp: value:"stream server grpc "
2021/11/17 13:26:44 resp: value:"stream server grpc "
1
2
go复制代码$ go run client.go
2021/11/17 13:26:44 resp err: value:"say.hello"

Bidirectional streaming RPC:双向流式 RPC

双向流式 RPC,由客户端以流式的方式发起请求,服务端也以流式的方式响应请求。

首个请求一定是 Client 发起,但具体交互方式(谁先谁后、一次发多少、响应多少、什么时候关闭)根据程序编写的方式来确定(可以结合协程)。

Proto :

1
2
3
4
5
6
7
8
9
10
11
go复制代码syntax = "proto3";

package proto;

message String {
string value = 1;
}

service HelloService {
rpc Hello (stream String) returns (stream String){};
}

server:

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
go复制代码package main

import (
"google.golang.org/grpc"
"io"
"log"
"net"
pb "rpc/proto" // 设置引用别名
)

// HelloServiceImpl 定义我们的服务
type HelloServiceImpl struct{}

//实现Hello方法
func (p *HelloServiceImpl) Hello(stream pb.HelloService_HelloServer) error {
for {
_ = stream.Send(&pb.String{Value: "say.hello"})

resp, err := stream.Recv()
//接收完了返回
if err == io.EOF {
return nil
}
if err != nil {
return err
}
log.Printf("resp: %v", resp)
}
}

func main() {
// 新建gRPC服务器实例
grpcServer := grpc.NewServer()
// 在gRPC服务器注册我们的服务
pb.RegisterHelloServiceServer(grpcServer, new(HelloServiceImpl))

lis, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal(err)
}
log.Println(" net.Listing...")
err = grpcServer.Serve(lis)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}

client:

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
go复制代码package main

import (
"context"
"google.golang.org/grpc"
"io"
"log"
pb "rpc/proto" // 设置引用别名
)

// SayHello 调用服务端的 Hello 方法
func SayHello(client pb.HelloServiceClient, r *pb.String) error {
stream, _ := client.Hello(context.Background())
for n := 0; n <= 3; n++ {
_ = stream.Send(r)
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}

log.Printf("resp err: %v", resp)
}

_ = stream.CloseSend()

return nil
}


func main() {
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
log.Fatal("dialing err:", err)
}
defer conn.Close()

// 建立gRPC连接
client := pb.NewHelloServiceClient(conn)

// 创建发送结构体
req := pb.String{
Value: "stream server grpc ",
}
SayHello(client, &req)
}

服务端在循环中接收客户端发来的数据,如果遇到io.EOF表示客户端流被关闭,如果函数退出表示服
务端流关闭。生成返回的数据通过流发送给客户端,双向流数据的发送和接收都是完全独立的行为。需
要注意的是,发送和接收的操作并不需要一一对应,用户可以根据真实场景进行组织代码。

开启服务器端,开启客户端。执行结果如下:

1
2
3
4
5
6
go复制代码$ go run server.go
2021/11/17 15:46:10 net.Listing...
2021/11/17 15:46:19 resp: value:"stream server grpc "
2021/11/17 15:46:19 resp: value:"stream server grpc "
2021/11/17 15:46:19 resp: value:"stream server grpc "
2021/11/17 15:46:19 resp: value:"stream server grpc "
1
2
3
4
5
go复制代码$ go run client.go
2021/11/17 15:46:19 resp err: value:"say.hello"
2021/11/17 15:46:19 resp err: value:"say.hello"
2021/11/17 15:46:19 resp err: value:"say.hello"
2021/11/17 15:46:19 resp err: value:"say.hello"

本文转载自: 掘金

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

浅谈MySQL事务特性与隔离级别 一、知识结构脑图 二、事务

发表于 2021-11-18

一、知识结构脑图

下面是我对事务知识总计的脑图,也是事务的主要内容,事务的特性,事务的实现以及事务的隔离级别

脑图

二、事务的基本特性(ACID)

定义:

  1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏。包括一致性读和一致性写。比如A向B转账,不可能A扣了钱,B却没收到。
  3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

目的:

  • 为数据库提供了一个从失败中恢复到正常状态的方法,同时提供了数据库在异常状态下仍保持一致性的方法
  • 为多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离的方法,以防止彼此之间的操作相互干扰

成功的情况下:

  • 能够将数据从一种状态变为另一种状态,并且能够持久化

异常情况下:

  • 能将数据恢复到正常状态
  • 要能够保持一致性,包含数据的一致性和约束的一致性

并发的情况下:

  • 并发的操作之间不会产生影响

总结:
事务的目的就是要提供3种方法:失败恢复的方法,保持一致的方法,操作隔离的方法

三、事务的并发问题

  1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读和幻读的区别

很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复 读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

四、MySQL事务隔离级别

不同隔离级别解决的问题:

事务隔离级别 脏读 不可重复读 幻读
读未提交 (Read-Uncommitted) 未解决 未解决 未解决
读已提交(Read-Committed) 解决 未解决 未解决
可重复度(Repeatable-Read) 解决 解决 未解决
串行化(Serializable) 解决 解决 解决
隔离级别越低,事务请求的锁越少或者保持锁的时间就越短。这也就是大多数数据库的隔离级别是READ COMMITTED的原因,MySQL的默认隔离级别就是Repeatable-Read。
MySQL8.0事务隔离级别为:REPEATABLE-READ

image.png

【参考】

【1】www.cnblogs.com/huanongying…

【2】《MySQL技术内幕 InnoDB存储引擎》

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

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

分布式配置系统Apollo如何实时更新配置的 引言 Apol

发表于 2021-11-18

引言

记得我们那时候刚开始学习Java的时候都只是一个单体项目,项目里面的配置基本都是写在项目里面的properties文件中,比如数据库配置啥的,各种逻辑开关,一旦这些配置修改了,还需要重启项目这修改才会生效。随着各种微服务的诞生,服务的拆分也越来越细,可能涉及的服务成千上百,服务基本也是集群部署,这样再去一个一个项目修改配置,然后重启这显然是行不通的。所以分布式配置中心就诞生了,现在开源的分布式配置中心也挺多的比如:开源分布式配置中心有很多,比如spring-cloud/spring-cloud-config、淘宝/diamond、百度/disconf、携程/apollo、netflix/archaius、Qconf、XDiamond、nacos等等。我们是不是很好奇配置中心如何做到实时更新并且通知到客户端的这也是一个面试中经常会问到的题目。下面我们就以apollo为例吧去分析分析它是如何实现的。为什么选择Apollo来分析列?因为现在的公司就在使用它作为配置中心。虽然Apollo是携程开源的,但是携程内部也不用它。

Apoll简介

要去了解一个玩意,就要先会去使用它。它的使用基本上很简单。虽然使用简单方便,但是它的设计还是挺复杂的,下面我们看一个它官网提供的架构图,是不是挺复杂的。在这里插入图片描述
通过上述架构图我们可以看到ConfigService、AdminService、Client、Portal、 Meta Server、Eureka这几个模块,主要的还是前面四个模块Meta Server、Eureka这两个模块只是Apollo本身内部所需要的辅助模块,我们暂时可以不需要关注它。

ConfigService

  • 提供配置获取接口
  • 提供配置推送接口
  • 服务于Apollo客户端

AdminService

  • 提供配置管理接口
  • 提供配置修改发布接口
  • 服务于管理界面Portal

Client

  • 为应用获取配置,支持实时更新
  • 通过MetaServer获取ConfigService的服务列表\
  • 使用客户端软负载SLB方式调用ConfigService

Portal

  • 配置管理界面
  • 通过MetaServer获取AdminService的服务列表
  • 使用客户端软负载SLB方式调用AdminService

Apoll更新配置

介绍完了上面这些Apollo组成的模块回到正题,配置中心如何做到实时更新并且到客户端如何感知配置被更新了?看这个问题之前我们先回顾下每到周末我们去人气比较旺的餐厅吃饭的时候流程是什么样的?

  • 首先肯定是现场取号,或者手机端取号。
  • 然后就是排队刷手机等着被叫号。中途你还会主动问一问还要等多久,服务员会告诉你等着吧,你前面还有几桌。
    上面这个吃饭的例子怎么知道到号了列?两种方式,一种使我们我每隔一段时间然后主动去问下服务员,是否到号,没到号继续刷手机,如果到号直接进去吃饭,还有一种的话就是干脆一直坐在那里刷手机我反正不赶时间,等着被通知到号。同样的配置中心的更新是如何通知到客户端列?是服务端主(configService)动通知到客户端(client)告诉它某某你的应用的配置被修改了,原来的值是啥被修改后的值是啥?还是说客户端(Client)每隔多久去问下服务端我的配置有没有被修改呀?如果是你你会怎么选择列?你也许会说我肯定两种方式都要呀!小朋友才会做选择?
    在这里插入图片描述

客户端长链接获取配置更新通知

再回到我们使用apollo的时候我们应用里面引入的Apollo的Client在我们应用启动的时候会有一个线程每隔5s向服务短发起一个http请求,不过这个http请求是不会立即返回的。它是一个长链接如果配置没有被更新,这个请求会被阻塞挂起,这个实现挂起的方式是通过Spring的DeferredResult来实现的,如果对这个Spring的DeferredResult不是很了解的推荐看下这个文章《5种SpringMvc的异步处理方式你都了解吗?》
挂起60s后会返回HTTP状态码为304给到客户端,如果再阻塞的过程中服务端配置有更新,这个Http请求会立马返回,并且把变化的nameSpace信息返回出去,并且返回的http的状态码是200。客户端得到状态码是200并且会根据nameSpace立即去服务端拉取最新的配置。
在这里插入图片描述
这里其实有一个问题,为什么不直接在长链接中返回变更后的结果,而是返回一个变更的通知,需要客户端根据这个变更通知立即去拉取新的配置?

感兴趣的可以参考下这个issue :github.com/apolloconfi…
这样推送消息就是有状态了,做不到幂等了,会带来很多问题。目前推送是单连接走http的,所以问题可能不大,不过在设计上而言是有这个问题的,比如如果推送是走的tcp长连接的话。另外,长轮询和推送之间也会有冲突,如果连续两次配置变化,就可能造成双写。还有一点,就是保持逻辑的简单,目前的做法推送只负责做简单的通知,不需要去计算客户端的配置应该是什么,因为计算逻辑挺复杂的,需要考虑集群,关联,灰度等,总而言之,就是在满足幂等性,实时性的基础上保持设计的简单。

客户端定时任务全量拉取配置

这样是不是就是很完美了,客户端可以实时接收到配置的更新。但是客户端如果接收服务端的更新内容处理失败,比如服务异常或者空指针的时候。这时候我们的客户端配置如果不重启是不是永远都不会被更新了。没关系这种情况apollo也帮你想到啦,你既然告诉我更新失败,那我就自己每隔一段时间主动去把我所有的配置都拉到客服端,拉回客服端之后和客户端的缓存配置做比较,如果一致直接结束,不一致就更新客户端的缓存,并且还会去异步更新本地文件。通过定时任务的补充,可以让配置达到最终的一致性。

客户端本地文件缓存配置

主动轮询,和定时任务全量拉取配置是不是就万无一失呢?只要涉及到分布式我们就要考虑到其他系统的宕机,比如哪一天挖机直接把部署Apollo的机房的光纤给挖断了,这样整个配置服务直接挂了,这时候主动轮询以及定时任务都没法起到作用了。是不是拉取不了配置,整个我们的客户端应用也要跟着受影响列,我们的配置基本上是改动的频率也是比较小的,即使我们的配置中心挂掉了,我们还有一份本地文件系统来兜底,这个文件目录默认是/opt/data或C:\opt\data,
在这里插入图片描述
所以即使配置中心挂了,对应用的影响也比较小。因为它还会去读取本地文件来兜底。

小结

到现在为止我们应该知道Apollo客户端是如何感知服务端配置更新了的把?

  • 主要是通过客户端应用发起一个长连接去Apollo ConfigServer端,如果Apollo ConfigServer端有配置更改会告诉应用端有配置修改,让客户端立马去拉取全量的配置,并且把配置更新到本地缓存,并且还会异步去更新本地文件缓存。
  • 客户端还有一个默认5min执行一次的定时任务,去拉取全量的配置。拉回配置之后也是对比本地缓存和远程是否一致,如果不一致则更新本地进程缓存为远程的,同时还去异步更新下本地文件。

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。
  • 站在巨人的肩膀
    www.apolloconfig.com/#/zh/design…
    www.iocoder.cn/Apollo/clie…

本文转载自: 掘金

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

从零开始学设计模式(十一):组合模式(Composite P

发表于 2021-11-18

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

作者的其他平台:

| CSDN:blog.csdn.net/qq\_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概3355字,读完共需10分钟

定义:

组合模式(Composite Pattern)又叫做部分-整体模式,它在树型结构(可以想象一下数据结构中的树)的问题中,模糊了简单元素和复杂元素的概念,客户端程序可以像处理简单元素一样来处理复杂元素,而使得客户端程序与复杂元素的内部结构进行解藕。

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点。

如下图的工程目录结构就是一个树型结构:

它可以表示为下面的树型结构图:

由上面的结构图可以发现,根节点和树枝节点其实在本质上属于同一种数据类型,它们都可以作为容器使用;但是叶子节点与树枝节点其实在语义上是不属于同一种类型。但是在组合模式中,是把树枝节点和叶子节点看作属于同一种数据类型(这里是用统一接口定义),从而使得让它们具备一致行为。

所以在组合模式中,整个的树形结构中的对象就会都属于同一种类型,这样的好处就是不需要用户来辨别是树枝节点还是叶子节点,就可以直接进行操作,给用户的使用带来极大的便利。

组成部分:

通过上述对于组合模式的描述,可以总结出组合模式的几个组成部分:

1、Component: 组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件,不管是组合还是叶结点。

2、Leaf: 在组合中表示叶子结点对象,叶子结点没有子结点。

3、Composite: 容器对象,表示参加组合的有子对象的对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加和删除等。

栗子

用上面文件夹的例子,首先声明一个文件夹相关的Component,FolderComponent:

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

private String name;

public String getName() {
return name;
}

public void setName(final String name) {
this.name = name;
}

public FolderComponent() {
}

public FolderComponent(final String name) {
this.name = name;
}

public abstract void add(FolderComponent component);

public abstract void remove(FolderComponent component);

public abstract void display();
}

再声明一个叶子节点文件夹,定义为FileLeaf,并且继承上面的FolderComponent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public class FileLeaf  extends FolderComponent{
public FileLeaf(final String name) {
super(name);
}

@Override
public void add(FolderComponent component) {
}

@Override
public void remove(FolderComponent component) {
}

@Override
public void display() {
System.out.println("叶子文件:" + this.getName());
}
}

最后声明一个容器对象,FolderComposit

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复制代码public class FolderComposite extends FolderComponent {
private final List<FolderComponent> components;

public FolderComposite(final String name) {
super(name);
this.components = new ArrayList<FolderComponent>();
}

public FolderComposite() {
this.components = new ArrayList<FolderComponent>();
}

@Override
public void add(final FolderComponent component) {
this.components.add(component);
}

@Override
public void remove(final FolderComponent component) {
this.components.remove(component);
}

@Override
public void display() {
System.out.println("文件夹组合容器的名字是" + this.getName());
for (final FolderComponent component : components) {
System.out.println("文件夹组合容器的当前文件夹是" + component.getName());
}
}
}

测试方法:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class CompositePatternTest {
public static void main(final String[] args) {
final FolderComponent leaf = new FileLeaf("叶子文件");
leaf.display();

final FolderComponent folder = new FolderComposite("文件夹一");
folder.add(new FileLeaf("文件夹里面的文件二"));
folder.add(new FileLeaf("文件夹里面的文件三"));
folder.display();
}
}

运行结果如下:

通过上面的例子可以总结出组合模式的优点和缺点。

组合模式的优点

1、组合模式使得客户端代码可以一致地处理单个对象和组合对象,从而无须关心自己处理的是单个对象,还是组合对象,大大地简化了代码结构;

2、组合模式的使用使得更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,实现了设计模式原则的“开闭原则”;

组合模式的缺点

1、使用组合模式需要花较多地时间理清类之间的层次关系,所以设计较复杂;

2、组合模式理解和设计较为复杂,增加代码难度;

3、如果使用了组合模式就不容易限制容器中的构件;

4、组合模式中不容易用继承的方法来增加构件的新功能;

使用场景

组合模式适用于以下情况:

1、当想要表示对象的部分-整体层次结构

2、当想要用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

总结

使用组合模式方便解耦客户端与复杂元素的内部结构,从而使得客户端程序可以像处理简单元素一样来一致处理复杂元素,大大简化了代码。对于层次结构的系统对象,可以使用组合模式进行处理。

相关推荐:

从零开始学设计模式(一):什么是设计模式

从零开始学设计模式(二):单例模式

从零开始学设计模式(三):原型模式(Prototype Pattern)

从零开始学设计模式(四):工厂模式(Factory Pattern)

从零开始学设计模式(五):建造者模式(Builder Pattern)

从零开始学设计模式(六):适配器模式(Adapter Pattern)

从零开始学设计模式(六):代理模式(Proxy Pattern)

从零开始学设计模式(八):装饰器模式(Decorator Pattern)

从零开始学设计模式(九):外观模式(Facade Pattern)

从零开始学设计模式(十):桥接模式(Bridge Pattern)

本文转载自: 掘金

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

redo Log 的持久化过程

发表于 2021-11-18

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

redo Log 的刷盘规则:

在MySQL引擎中,redo log 是用来保证事务的原子性和持久性的特性。

今天我们来具体看看,对于redo log中 如何进行将日志缓存 redo log Buffer 写入 redo log file 中,

一般情况下,对于通常的事务提交,分为三个阶段:

  • 事务准备提交:
  • 事务提交过程中:
  • 事务提交完成:
  • 主要过程是,在事务提交时候, 会发生强制的将 redo log buffer的日志缓存的数据,强制写入 redo log file中, 通常会调用一次操作系统的fsync()的操作。*

其中还会经过操作系统的内核空间, OS buffer ,因为MySQL的进行和日志缓存都工作在操作系统中环境下

本质:

事务提交的过程中,必须将日志缓存的数据持久化到磁盘的日志文件中,期间还需要经过操作系统的 “内核空间缓存区”–,也就是OS Buffer区域,

Redo log从用户空间的 Log buffer 写入磁盘的Redo Log文件时,

需要要内核空间的OS buffer;

日志文件,没有使用 O_DIRECT标识,如果有这个标识,就可以不经过这个os buffer的内核空间,直接写入磁盘数据,;

注意事项:

对于redo log 保持持久性是,必须要将日志缓存,写入磁盘,通常这个 redo buffer 会检测当前数据又多少,如果超过一半,才会触发 刷盘操作,也就是会进行持久化 写入磁盘数据的操作;

当时事务存在检查点的时候,代表了刷写到磁盘日志所处的LSN的位置, 日志的逻辑序列号;

redo log 从日志 redo buffer 到 redo log file 持久化的示意图

从日志缓存持久化到 内核空间os buffer —>file文件中;

image.png

本文转载自: 掘金

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

1…287288289…956

开发者博客

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