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

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


  • 首页

  • 归档

  • 搜索

高级JAVA开发必备技能:java8 新日期时间API((二

发表于 2021-11-02

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

❤️作者简介:Java领域优质创作者🏆,CSDN博客专家认证🏆,华为云享专家认证🏆

❤️技术活,该赏

❤️点赞 👍 收藏 ⭐再看,养成习惯

大家好,我是小虚竹。之前有粉丝私聊我,问能不能把JAVA8 新的日期时间API(JSR-310)知识点梳理出来。答案是肯定的,谁让我宠粉呢。由于内容偏多,会拆成多篇来写。

闲话就聊到这,请看下面的正文。

常用的日期时间API简介

介绍下java8API比较常用的日期时间API,按java.time 包的类顺序:

  • Clock:时钟
  • Instant:瞬间时间。
  • LocalDate:本地日期。只有表示年月日
  • LocalDateTime:本地日期时间,LocalDate+LocalTime
  • LocalTime:本地时间,只有表示时分秒
  • OffsetDateTime:有时间偏移量的日期时间(不包含基于ZoneRegion的时间偏移量)
  • OffsetTime:有时间偏移量的时间
  • ZonedDateTime:有时间偏移量的日期时间(包含基于ZoneRegion的时间偏移量)

博主把这些类都点开看了,都是属于不可变类。而且官方也说了,java.time包 下的类都是线程安全的。

Clock

Clock类说明

1
2
3
java复制代码public abstract class Clock {
...
}

Clock 是抽象类,内部提供了四个内部类,这是它的内部实现类

image-2021081496702

  • FixedClock :始终返回相同瞬间的时钟,通常使用于测试。
  • OffsetClock :偏移时钟,时间偏移量的单位是Duration。
  • SystemClock :系统默认本地时钟。
  • TickClock :偏移时钟,时间偏移量的单位是纳秒。

Clock 提供了下面这几个常用的方法(这几个方法在实现类里都有对应的实现):

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 获取时钟的当前Instant对象。
public abstract Instant instant()

// 获取时钟的当前毫秒数值
public long millis()

// 获取用于创建时钟的时区。
public abstract ZoneId getZone()

// 返回具有指定时区的当前时钟的新实例
public abstract Clock withZone(ZoneId zone)

FixedClock

Clock.fixed

1
java复制代码public static Clock fixed(Instant fixedInstant, ZoneId zone)

需要传递instant和zone,并将返回具有固定瞬间的时钟。

1
2
3
4
5
java复制代码		Instant instant = Instant.now();
Clock fixedClock = Clock.fixed(instant, ZoneId.of("Asia/Shanghai"));
Clock fixedClock1 = Clock.fixed(instant, ZoneId.of("GMT"));
System.out.println("中国时区的Clock:"+fixedClock);
System.out.println("GMT时区的Clock:"+fixedClock1);

image-20210814195855581

由运行结果可知,返回的结果是有带对应时区的。

验证获取的时钟会不会改变:

1
2
3
4
5
6
7
8
9
java复制代码		Clock clock = Clock.systemDefaultZone();
Clock fixedClock = Clock.fixed(clock.instant(), ZoneId.of("Asia/Shanghai"));
System.out.println(fixedClock.instant());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(fixedClock.instant());

image-202108149044323

Clock.fixed 创建一个固定的时钟,clock 对象将始终提供与指定相同的时刻。。如图所示,强制睡眠1秒,但是时刻没变。

Clock.fixed 跟 Offset 方法更配

由上面可知Clock.fixed 得到一个固定的时钟,那要添加时间或者减去时间就要用到Offset 方法

示例代码如下

1
2
3
4
5
6
7
8
java复制代码		Clock clock = Clock.systemDefaultZone();
Clock fixedClock = Clock.fixed(clock.instant(), ZoneId.of("Asia/Shanghai"));
System.out.println(fixedClock.instant());
Clock clockAdd = Clock.offset(clock, Duration.ofMinutes(20));
Clock clockSub = Clock.offset(clock, Duration.ofMinutes(-10));
System.out.println("原先的: " + clock.instant());
System.out.println("加了20分钟: " + clockAdd.instant());
System.out.println("减了10分钟: " + clockSub.instant());

image-202108141995813

OffsetClock

OffsetClock 是偏移时钟,时间偏移量的单位是Duration。

1
2
3
4
5
6
7
8
9
java复制代码//Clock
public static Clock offset(Clock baseClock, Duration offsetDuration) {
Objects.requireNonNull(baseClock, "baseClock");
Objects.requireNonNull(offsetDuration, "offsetDuration");
if (offsetDuration.equals(Duration.ZERO)) {
return baseClock;
}
return new OffsetClock(baseClock, offsetDuration);
}

由源码可知,使用Clock.offset方法 返回的是OffsetClock实例对象

1
2
3
4
5
6
java复制代码		Clock clock = Clock.systemDefaultZone();
Clock fixedClock = Clock.fixed(clock.instant(), ZoneId.of("Asia/Shanghai"));
System.out.println(fixedClock.instant());
Clock clockAdd = Clock.offset(clock, Duration.ofMinutes(20));
System.out.println("原先的: " + clock.instant());
System.out.println("加了20分钟: " + clockAdd.instant());

image-20210814944060

SystemClock

SystemClock 是系统默认的本地时钟。

1
2
3
4
5
java复制代码		Clock clock = Clock.systemDefaultZone();
System.out.println(clock.millis());
Clock utc = Clock.systemUTC();
System.out.println(utc.millis());
System.out.println(System.currentTimeMillis());

image-20210814904947

居然完全一样。这就要看下源码了

Clock.systemDefaultZone()

用的是系统默认的时区ZoneId.systemDefault()

1
2
3
csharp复制代码    public static Clock systemDefaultZone() {
return new SystemClock(ZoneId.systemDefault());
}

image-2021081495878

最终调用的也是System.currentTimeMillis()

Clock.systemUTC()

用的是UTC时区ZoneOffset.UTC

1
2
3
java复制代码    public static Clock systemUTC() {
return new SystemClock(ZoneOffset.UTC);
}

image-2021081495878

最终调用的也是System.currentTimeMillis()

结论

Clock.systemDefaultZone() 和Clock.systemUTC()获取的millis()时间戳是一样的,就是对应时区的差别。

TickClock

TickClock 是偏移时钟,时间偏移量的最小单位是纳秒。

如图所示,Clock主要提供下面三个方法

1
2
3
4
5
6
java复制代码//构造的时钟的计时单位是自定义的偏移量单位
public static Clock tick(Clock baseClock, Duration tickDuration);
//构造的时钟的计时单位是分
public static Clock tickMinutes(ZoneId zone);
//构造的时钟的计时单位是秒
public static Clock tickSeconds(ZoneId zone) ;

image-202108149595

实战:

1
2
3
4
5
6
7
8
9
10
11
java复制代码		Clock tickClock = Clock.tick(Clock.systemDefaultZone(),Duration.ofHours(1L));
Clock tickMinutes = Clock.tickMinutes(ZoneId.of("Asia/Shanghai"));
Clock tickSeconds = Clock.tickSeconds(ZoneId.of("Asia/Shanghai"));

LocalDateTime tickClockLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(tickClock.millis()),ZoneId.of("Asia/Shanghai"));
LocalDateTime tickMinutesLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(tickMinutes.millis()),ZoneId.of("Asia/Shanghai"));
LocalDateTime tickSecondsLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(tickSeconds.millis()),ZoneId.of("Asia/Shanghai"));

System.out.println("tickClock :"+tickClock.millis() +" 转为date时间:"+tickClockLocalDateTime);
System.out.println("tickMinutes:"+tickMinutes.millis() +" 转为date时间:"+tickMinutesLocalDateTime);
System.out.println("tickSeconds:"+tickSeconds.millis() +" 转为date时间:"+tickSecondsLocalDateTime);

偏移量的单位支持:天,时,分,秒,豪秒,纳秒

image-20210814909314

image-2021081495696

Instant

Instant类说明

1
2
3
4
java复制代码public final class Instant
implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable {
...
}

Instant表示瞬间时间。也是不可变类且是线程安全的。其实Java.time 这个包是线程安全的。

Instant是java 8新增的特性,里面有两个核心的字段

1
2
3
4
5
java复制代码	...	
private final long seconds;

private final int nanos;
...

一个是单位为秒的时间戳,另一个是单位为纳秒的时间戳。

是不是跟**System.currentTimeMillis()**返回的long时间戳很像,System.currentTimeMillis()返回的是毫秒级,Instant多了更精确的纳秒级时间戳。

Instant常用的用法

1
2
3
4
java复制代码 		Instant now = Instant.now();
System.out.println("now:"+now);
System.out.println(now.getEpochSecond()); // 秒
System.out.println(now.toEpochMilli()); // 毫秒

image-20210720905353

Instant是没有时区的,但是Instant加上时区后,可以转化为ZonedDateTime

1
2
3
java复制代码		Instant ins = Instant.now();
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt);

image-202107205211996

long型时间戳转Instant

要注意long型时间戳的时间单位选择Instant对应的方法转化

1
2
3
4
5
6
7
8
java复制代码//1626796436 为秒级时间戳
Instant ins = Instant.ofEpochSecond(1626796436);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println("秒级时间戳转化:"+zdt);
//1626796436111l 为秒级时间戳
Instant ins1 = Instant.ofEpochMilli(1626796436111l);
ZonedDateTime zdt1 = ins1.atZone(ZoneId.systemDefault());
System.out.println("毫秒级时间戳转化:"+zdt1);

Instant的坑

Instant.now()获取的时间与北京时间相差8个时区,这是一个细节,要避坑。

看源码,用的是UTC时间。

1
2
3
java复制代码public static Instant now() {
return Clock.systemUTC().instant();
}

解决方案:

1
2
java复制代码Instant now = Instant.now().plusMillis(TimeUnit.HOURS.toMillis(8));
System.out.println("now:"+now);

image-202107234326190

LocalDate

LocalDate类说明

LocalDate表示本地日期。只有表示年月日。相当于:yyyy-MM-dd。

LocalDate常用的用法

获取当前日期

1
2
3
4
5
6
7
java复制代码		LocalDate localDate1 = LocalDate.now();
LocalDate localDate2 = LocalDate.now(ZoneId.of("Asia/Shanghai"));
LocalDate localDate3 = LocalDate.now(Clock.systemUTC());

System.out.println("now :"+localDate1);
System.out.println("now by zone :"+localDate2);
System.out.println("now by Clock:"+localDate3);

image-2021081496781

获取localDate对象

1
2
3
4
java复制代码		LocalDate localDate1 = LocalDate.of(2021, 8, 14);
LocalDate localDate2 = LocalDate.parse("2021-08-14");
System.out.println(localDate1);
System.out.println(localDate2);

image-2021081497325

获取指定日期的年月日

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码		LocalDate localDate1 = LocalDate.of(2021, 8, 14);
// 当前日期年份:2021
System.out.println(localDate1.getYear());
// 当前日期月份对象:AUGUST
System.out.println(localDate1.getMonth());
// 当前日期月份:8
System.out.println(localDate1.getMonthValue());
// 该日期是当前周的第几天:6
System.out.println(localDate1.getDayOfWeek().getValue());
// 该日期是当前月的第几天:14
System.out.println(localDate1.getDayOfMonth());
// 该日期是当前年的第几天:226
System.out.println(localDate1.getDayOfYear());

image-2021081498430

修改年月日

1
2
3
4
5
6
7
java复制代码		LocalDate localDate1 = LocalDate.of(2021, 8, 14);
// 修改该日期的年份:2022-08-14
System.out.println(localDate1.withYear(2022));
// 修改该日期的月份:2021-12-14
System.out.println(localDate1.withMonth(12));
// 修改该日期在当月的天数:2021-08-01
System.out.println(localDate1.withDayOfMonth(1));

image-20210814935404

比较日期

1
2
3
4
5
6
7
8
9
10
css复制代码		LocalDate localDate1 = LocalDate.of(2021, 8, 14);
// 比较指定日期和参数日期,返回正数,那么指定日期时间较晚(数字较大):13
int i = localDate1.compareTo(LocalDate.of(2021, 8, 1));
System.out.println(i);
// 比较指定日期是否比参数日期早(true为早):true
System.out.println(localDate1.isBefore(LocalDate.of(2021,8,31)));
// 比较指定日期是否比参数日期晚(true为晚):false
System.out.println(localDate1.isAfter(LocalDate.of(2021,8,31)));
// 比较两个日期是否相等:true
System.out.println(localDate1.isEqual(LocalDate.of(2021, 8, 14)));

image-202108149597

LocalDate 和String相互转化、Date和LocalDate相互转化

LocalDate 和String相互转化

1
2
3
4
5
6
7
8
9
10
java复制代码		LocalDate localDate1 = LocalDate.of(2021, 8, 14);
// LocalDate 转 String
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String dateString = localDate1.format(dateTimeFormatter);
System.out.println("LocalDate 转 String:"+dateString);
// String 转 LocalDate
String str = "2021-08-14";
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse(str, fmt);
System.out.println("String 转 LocalDate:"+date);

image-2021081499979

Date和LocalDate相互转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码	// Date 转 LocalDate
Date now = new Date();
// 先将Date转换为ZonedDateTime
Instant instant = now.toInstant();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("Asia/Shanghai"));
LocalDate localDate = zonedDateTime.toLocalDate();
// Sat Aug 14 23:16:28 CST 2021
System.out.println(now);
// 2021-08-14
System.out.println(localDate);

// LocalDate 转 Date
LocalDate now1 = LocalDate.now();
ZonedDateTime dateTime = now1.atStartOfDay(ZoneId.of("Asia/Shanghai"));
Date date1 = Date.from(dateTime.toInstant());
System.out.println(date1);

image-2021081492237

LocalDateTime

LocalDateTime类说明

表示当前日期时间,相当于:yyyy-MM-ddTHH:mm:ss

LocalDateTime常用的用法

获取当前日期和时间

1
2
3
4
5
6
java复制代码		LocalDate d = LocalDate.now(); // 当前日期
LocalTime t = LocalTime.now(); // 当前时间
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
System.out.println(d); // 严格按照ISO 8601格式打印
System.out.println(t); // 严格按照ISO 8601格式打印
System.out.println(dt); // 严格按照ISO 8601格式打印

image-20210714857780

由运行结果可行,本地日期时间通过now()获取到的总是以当前默认时区返回的

获取指定日期和时间

1
2
3
4
5
6
java复制代码		LocalDate d2 = LocalDate.of(2021, 07, 14); // 2021-07-14, 注意07=07月
LocalTime t2 = LocalTime.of(13, 14, 20); // 13:14:20
LocalDateTime dt2 = LocalDateTime.of(2021, 07, 14, 13, 14, 20);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);
System.out.println("指定日期时间:"+dt2);
System.out.println("指定日期时间:"+dt3);

image-20210714803165

日期时间的加减法及修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码		LocalDateTime currentTime = LocalDateTime.now(); // 当前日期和时间
System.out.println("------------------时间的加减法及修改-----------------------");
//3.LocalDateTime的加减法包含了LocalDate和LocalTime的所有加减,上面说过,这里就只做简单介绍
System.out.println("3.当前时间:" + currentTime);
System.out.println("3.当前时间加5年:" + currentTime.plusYears(5));
System.out.println("3.当前时间加2个月:" + currentTime.plusMonths(2));
System.out.println("3.当前时间减2天:" + currentTime.minusDays(2));
System.out.println("3.当前时间减5个小时:" + currentTime.minusHours(5));
System.out.println("3.当前时间加5分钟:" + currentTime.plusMinutes(5));
System.out.println("3.当前时间加20秒:" + currentTime.plusSeconds(20));
//还可以灵活运用比如:向后加一年,向前减一天,向后加2个小时,向前减5分钟,可以进行连写
System.out.println("3.同时修改(向后加一年,向前减一天,向后加2个小时,向前减5分钟):" + currentTime.plusYears(1).minusDays(1).plusHours(2).minusMinutes(5));
System.out.println("3.修改年为2025年:" + currentTime.withYear(2025));
System.out.println("3.修改月为12月:" + currentTime.withMonth(12));
System.out.println("3.修改日为27日:" + currentTime.withDayOfMonth(27));
System.out.println("3.修改小时为12:" + currentTime.withHour(12));
System.out.println("3.修改分钟为12:" + currentTime.withMinute(12));
System.out.println("3.修改秒为12:" + currentTime.withSecond(12));

image-20210714941902

LocalDateTime和Date相互转化

Date转LocalDateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码		System.out.println("------------------方法一:分步写-----------------------");
//实例化一个时间对象
Date date = new Date();
//返回表示时间轴上同一点的瞬间作为日期对象
Instant instant = date.toInstant();
//获取系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
//根据时区获取带时区的日期和时间
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
//转化为LocalDateTime
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
System.out.println("方法一:原Date = " + date);
System.out.println("方法一:转化后的LocalDateTime = " + localDateTime);

System.out.println("------------------方法二:一步到位(推荐使用)-----------------------");
//实例化一个时间对象
Date todayDate = new Date();
//Instant.ofEpochMilli(long l)使用1970-01-01T00:00:00Z的纪元中的毫秒来获取Instant的实例
LocalDateTime ldt = Instant.ofEpochMilli(todayDate.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
System.out.println("方法二:原Date = " + todayDate);
System.out.println("方法二:转化后的LocalDateTime = " + ldt);

image-20210714210839339

LocalDateTime转Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码		System.out.println("------------------方法一:分步写-----------------------");
//获取LocalDateTime对象,当前时间
LocalDateTime localDateTime = LocalDateTime.now();
//获取系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
//根据时区获取带时区的日期和时间
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
//返回表示时间轴上同一点的瞬间作为日期对象
Instant instant = zonedDateTime.toInstant();
//转化为Date
Date date = Date.from(instant);
System.out.println("方法一:原LocalDateTime = " + localDateTime);
System.out.println("方法一:转化后的Date = " + date);

System.out.println("------------------方法二:一步到位(推荐使用)-----------------------");
//实例化一个LocalDateTime对象
LocalDateTime now = LocalDateTime.now();
//转化为date
Date dateResult = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
System.out.println("方法二:原LocalDateTime = " + now);
System.out.println("方法二:转化后的Date = " + dateResult);

image-20210714211035080

LocalTime

LocalTime类说明

LocalTime:本地时间,只有表示时分秒

LocalTime常用的用法

获取当前时间

1
2
3
4
5
6
7
java复制代码		LocalTime localTime1 = LocalTime.now();
LocalTime localTime2 = LocalTime.now(ZoneId.of("Asia/Shanghai"));
LocalTime localTime3 = LocalTime.now(Clock.systemDefaultZone());

System.out.println("now :"+localTime1);
System.out.println("now by zone :"+localTime2);
System.out.println("now by Clock:"+localTime3);

image-2021081498171

获取LocalTime对象

1
2
3
4
java复制代码		LocalTime localTime1 = LocalTime.of(23, 26, 30);
LocalTime localTime2 = LocalTime.of(23, 26);
System.out.println(localTime1);
System.out.println(localTime2);

image-2021081494673

获取指定日期的时分秒

1
2
3
4
5
6
7
java复制代码		LocalTime localTime1 = LocalTime.of(23, 26, 30);
//当前时间的时:23
System.out.println(localTime1.getHour());
//当前时间的分:26
System.out.println(localTime1.getMinute());
//当前时间的秒:30
System.out.println(localTime1.getSecond());

image-2021081492055

修改时分秒

1
2
3
4
5
6
7
java复制代码		LocalTime localTime1 = LocalTime.of(23, 26, 30);
//修改时间的时:00:26:30
System.out.println(localTime1.withHour(0));
//修改时间的分:23:30:30
System.out.println(localTime1.withMinute(30));
//修改时间的秒:23:26:59
System.out.println(localTime1.withSecond(59));

image-202108149774

比较时间

1
2
3
4
5
6
7
8
9
10
11
java复制代码		LocalTime localTime1 = LocalTime.of(23, 26, 30);
LocalTime localTime2 = LocalTime.of(23, 26, 32);
// 两个时间进行比较 大返回1,小就返回-1,一样就返回0:-1
System.out.println(localTime1.compareTo(localTime2));

// 比较指定时间是否比参数时间早(true为早):true
System.out.println(localTime1.isBefore(localTime2));
// 比较指定时间是否比参数时间晚(true为晚):false
System.out.println(localTime1.isAfter(localTime2));
// 比较两个时间是否相等:true
System.out.println(localTime1.equals(LocalTime.of(23, 26, 30)));

image-2021081498214

OffsetDateTime

OffsetDateTime类说明

OffsetDateTime:有时间偏移量的日期时间(不包含基于ZoneRegion的时间偏移量)

1
2
3
4
5
6
7
8
java复制代码public final class OffsetDateTime
implements Temporal, TemporalAdjuster, Comparable<OffsetDateTime>, Serializable {
//The minimum supported {@code OffsetDateTime}, '-999999999-01-01T00:00:00+18:00'
public static final OffsetDateTime MIN = LocalDateTime.MIN.atOffset(ZoneOffset.MAX);
// The maximum supported {@code OffsetDateTime}, '+999999999-12-31T23:59:59.999999999-18:00'.
public static final OffsetDateTime MAX = LocalDateTime.MAX.atOffset(ZoneOffset.MIN);
...
}

上面的MIN 和MAX 是公有静态变量。

OffsetDateTime常用的用法

获取当前日期时间

1
2
3
4
5
6
7
java复制代码		OffsetDateTime offsetDateTime1 = OffsetDateTime.now();
OffsetDateTime offsetDateTime2 = OffsetDateTime.now(ZoneId.of("Asia/Shanghai"));
OffsetDateTime offsetDateTime3 = OffsetDateTime.now(Clock.systemUTC());

System.out.println("now :"+offsetDateTime1);
System.out.println("now by zone :"+offsetDateTime2);
System.out.println("now by Clock:"+offsetDateTime3);

image-2021082196097

获取OffsetDateTime对象

1
2
3
4
5
6
7
8
9
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
OffsetDateTime offsetDateTime2 = OffsetDateTime. of(2021, 8, 15, 13, 14, 20,0, ZoneOffset.ofHours(8));
Instant now = Instant.now();
OffsetDateTime offsetDateTime3 = OffsetDateTime.ofInstant(now, ZoneId.of("Asia/Shanghai"));

System.out.println(offsetDateTime1);
System.out.println(offsetDateTime2);
System.out.println(offsetDateTime3);

image-20210821900413

获取指定日期的年月日时分秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
//当前时间的年:2021
System.out.println(offsetDateTime1.getYear());
//当前时间的月:8
System.out.println(offsetDateTime1.getMonthValue());
//当前时间的日:15
System.out.println(offsetDateTime1.getDayOfMonth());
//当前时间的时:13
System.out.println(offsetDateTime1.getHour());
//当前时间的分:14
System.out.println(offsetDateTime1.getMinute());
//当前时间的秒:20
System.out.println(offsetDateTime1.getSecond());

image-2021082193542

修改年月日时分秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
//修改时间的年:2022-08-15T13:14:20+08:00
System.out.println(offsetDateTime1.withYear(2022));
//修改时间的月:2021-09-15T13:14:20+08:00
System.out.println(offsetDateTime1.withMonth(9));
//修改时间的日:2021-08-30T13:14:20+08:00
System.out.println(offsetDateTime1.withDayOfMonth(30));
//修改时间的时:2021-08-15T00:14:20+08:00
System.out.println(offsetDateTime1.withHour(0));
//修改时间的分:2021-08-15T13:30:20+08:00
System.out.println(offsetDateTime1.withMinute(30));
//修改时间的秒:2021-08-15T13:14:59+08:00
System.out.println(offsetDateTime1.withSecond(59));

image-2021082194524

比较日期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
OffsetDateTime offsetDateTime3 = OffsetDateTime.of(localDateTime1, ZoneOffset.ofHours(8));

LocalDateTime localDateTime2 = LocalDateTime.of(2021, 8, 15, 13, 14, 30);
OffsetDateTime offsetDateTime2 = OffsetDateTime.of(localDateTime2, ZoneOffset.ofHours(8));

// 两个时间进行比较 大返回1,小就返回-1,一样就返回0:-1
System.out.println(offsetDateTime1.compareTo(offsetDateTime2));

// 比较指定时间是否比参数时间早(true为早):true
System.out.println(offsetDateTime1.isBefore(offsetDateTime2));
// 比较指定时间是否比参数时间晚(true为晚):false
System.out.println(offsetDateTime1.isAfter(offsetDateTime2));
// 比较两个时间是否相等:true
System.out.println(offsetDateTime1.equals(offsetDateTime3));

image-20210821944542

字符串转化为OffsetDateTime对象

1
2
3
4
5
6
java复制代码				String str = "2021-08-15T10:15:30+08:00";
OffsetDateTime offsetDateTime1 = OffsetDateTime.parse(str);
OffsetDateTime offsetDateTime2 = OffsetDateTime.parse(str,DateTimeFormatter.ISO_OFFSET_DATE_TIME);

System.out.println(offsetDateTime1);
System.out.println(offsetDateTime2);

image-2021082196169

OffsetTime

OffsetTime类说明

OffsetTime:有时间偏移量的时间

1
2
3
4
5
6
7
8
9
java复制代码public final class OffsetTime
implements Temporal, TemporalAdjuster, Comparable<OffsetTime>, Serializable {
//The minimum supported {@code OffsetTime}, '00:00:00+18:00'.
public static final OffsetTime MIN = LocalTime.MIN.atOffset(ZoneOffset.MAX);

//The maximum supported {@code OffsetTime}, '23:59:59.999999999-18:00'.
public static final OffsetTime MAX = LocalTime.MAX.atOffset(ZoneOffset.MIN);
...
}

上面的MIN 和MAX 是公有静态变量。

OffsetTime常用的用法

获取当前时间

1
2
3
4
5
6
7
java复制代码		OffsetTime offsetTime1 = OffsetTime.now();
OffsetTime offsetTime2 = OffsetTime.now(ZoneId.of("Asia/Shanghai"));
OffsetTime offsetTime3 = OffsetTime.now(Clock.systemUTC());

System.out.println("now :"+offsetTime1);
System.out.println("now by zone :"+offsetTime2);
System.out.println("now by Clock:"+offsetTime3);

image-2021088203

获取OffsetTime对象

1
2
3
4
5
6
7
8
9
java复制代码		LocalTime localTime1 = LocalTime.of(13, 14, 20);
OffsetTime offsetTime1 = OffsetTime.of(localTime1, ZoneOffset.ofHours(8));
OffsetTime offsetTime2 = OffsetTime. of(13, 14, 20,0, ZoneOffset.ofHours(8));
Instant now = Instant.now();
OffsetTime offsetTime3 = OffsetTime.ofInstant(now, ZoneId.of("Asia/Shanghai"));

System.out.println(offsetTime1);
System.out.println(offsetTime2);
System.out.println(offsetTime3);

image-20210895380

获取指定时间的时分秒

1
2
3
4
5
6
7
8
9
java复制代码		LocalTime localTime1 = LocalTime.of( 13, 14, 20);
OffsetTime offsetTime1 = OffsetTime.of(localTime1, ZoneOffset.ofHours(8));

//当前时间的时:13
System.out.println(offsetTime1.getHour());
//当前时间的分:14
System.out.println(offsetTime1.getMinute());
//当前时间的秒:20
System.out.println(offsetTime1.getSecond());

image-202108802988

修改时分秒

1
2
3
4
5
6
7
8
9
java复制代码		LocalTime localTime1 = LocalTime.of( 13, 14, 20);
OffsetTime offsetTime1 = OffsetTime.of(localTime1, ZoneOffset.ofHours(8));

//修改时间的时:00:14:20+08:00
System.out.println(offsetTime1.withHour(0));
//修改时间的分:13:30:20+08:00
System.out.println(offsetTime1.withMinute(30));
//修改时间的秒:13:14:59+08:00
System.out.println(offsetTime1.withSecond(59));

image-202108945483

比较时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码		LocalTime localTime1 = LocalTime.of( 13, 14, 20);
OffsetTime offsetTime1 = OffsetTime.of(localTime1, ZoneOffset.ofHours(8));
OffsetTime offsetTime3 = OffsetTime.of(localTime1, ZoneOffset.ofHours(8));

LocalTime localTime2 = LocalTime.of(13, 14, 30);
OffsetTime offsetTime2 = OffsetTime.of(localTime2, ZoneOffset.ofHours(8));
// 两个时间进行比较 大返回1,小就返回-1,一样就返回0:-1
System.out.println(offsetTime1.compareTo(offsetTime2));

// 比较指定时间是否比参数时间早(true为早):true
System.out.println(offsetTime1.isBefore(offsetTime2));
// 比较指定时间是否比参数时间晚(true为晚):false
System.out.println(offsetTime1.isAfter(offsetTime2));
// 比较两个时间是否相等:true
System.out.println(offsetTime1.equals(offsetTime3));

image-2021089109890

ZonedDateTime

ZonedDateTime类说明

表示一个带时区的日期和时间,ZonedDateTime可以理解为LocalDateTime+ZoneId

从源码可以看出来,ZonedDateTime类中定义了LocalDateTime和ZoneId两个变量。

且ZonedDateTime类也是不可变类且是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public final class ZonedDateTime
implements Temporal, ChronoZonedDateTime<LocalDate>, Serializable {

/**
* Serialization version.
*/
private static final long serialVersionUID = -6260982410461394882L;

/**
* The local date-time.
*/
private final LocalDateTime dateTime;
/**
* The time-zone.
*/
private final ZoneId zone;

...
}

ZonedDateTime常用的用法

获取当前日期时间

1
2
3
4
5
6
7
8
9
java复制代码		// 默认时区获取当前时间
ZonedDateTime zonedDateTime = ZonedDateTime.now();
// 用指定时区获取当前时间,Asia/Shanghai为上海时区
ZonedDateTime zonedDateTime1 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
//withZoneSameInstant为转换时区,参数为ZoneId
ZonedDateTime zonedDateTime2 = zonedDateTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(zonedDateTime);
System.out.println(zonedDateTime1);
System.out.println(zonedDateTime2);

image-202107205246938

1
2
3
4
5
6
7
java复制代码		ZonedDateTime zonedDateTime1 = ZonedDateTime.now();
ZonedDateTime zonedDateTime2 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime zonedDateTime3 = ZonedDateTime.now(Clock.systemUTC());

System.out.println("now :"+zonedDateTime1);
System.out.println("now by zone :"+zonedDateTime2);
System.out.println("now by Clock:"+zonedDateTime3);

image-202108957912

获取ZonedDateTime对象

1
2
3
4
5
6
7
8
9
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
ZonedDateTime zonedDateTime2 = ZonedDateTime. of(2021, 8, 15, 13, 14, 20,0, ZoneOffset.ofHours(8));
Instant now = Instant.now();
ZonedDateTime zonedDateTime3 = ZonedDateTime.ofInstant(now, ZoneId.of("Asia/Shanghai"));

System.out.println(zonedDateTime1);
System.out.println(zonedDateTime2);
System.out.println(zonedDateTime3);

image-2021088020148

获取指定日期的年月日时分秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
//当前时间的年:2021
System.out.println(zonedDateTime1.getYear());
//当前时间的月:8
System.out.println(zonedDateTime1.getMonthValue());
//当前时间的日:15
System.out.println(zonedDateTime1.getDayOfMonth());
//当前时间的时:13
System.out.println(zonedDateTime1.getHour());
//当前时间的分:14
System.out.println(zonedDateTime1.getMinute());
//当前时间的秒:20
System.out.println(zonedDateTime1.getSecond());

image-202108219231845

修改年月日时分秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime1, ZoneOffset.ofHours(8));
//修改时间的年:2022-08-15T13:14:20+08:00
System.out.println(zonedDateTime1.withYear(2022));
//修改时间的月:2021-09-15T13:14:20+08:00
System.out.println(zonedDateTime1.withMonth(9));
//修改时间的日:2021-08-30T13:14:20+08:00
System.out.println(zonedDateTime1.withDayOfMonth(30));
//修改时间的时:2021-08-15T00:14:20+08:00
System.out.println(zonedDateTime1.withHour(0));
//修改时间的分:2021-08-15T13:30:20+08:00
System.out.println(zonedDateTime1.withMinute(30));
//修改时间的秒:2021-08-15T13:14:59+08:00
System.out.println(zonedDateTime1.withSecond(59));

image-20210821998

比较日期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码		LocalDateTime localDateTime1 = LocalDateTime.of(2021, 8, 15, 13, 14, 20);
ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime1, ZoneOffset.ofHours(8));

ZonedDateTime zonedDateTime3 = ZonedDateTime.of(localDateTime1, ZoneOffset.ofHours(8));

LocalDateTime localDateTime2 = LocalDateTime.of(2021, 8, 15, 13, 14, 30);
ZonedDateTime zonedDateTime2 = ZonedDateTime.of(localDateTime2, ZoneOffset.ofHours(8));

// 两个时间进行比较 大返回1,小就返回-1,一样就返回0:-1
System.out.println(zonedDateTime1.compareTo(zonedDateTime2));

// 比较指定时间是否比参数时间早(true为早):true
System.out.println(zonedDateTime1.isBefore(zonedDateTime2));
// 比较指定时间是否比参数时间晚(true为晚):false
System.out.println(zonedDateTime1.isAfter(zonedDateTime2));
// 比较两个时间是否相等:true
System.out.println(zonedDateTime1.equals(zonedDateTime3));

image-20210821907094

LocalDateTime+ZoneId变ZonedDateTime

1
2
3
4
5
java复制代码		LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime1 = localDateTime.atZone(ZoneId.systemDefault());
ZonedDateTime zonedDateTime2 = localDateTime.atZone(ZoneId.of("America/New_York"));
System.out.println(zonedDateTime1);
System.out.println(zonedDateTime2);

image-2021072094003

上面的例子说明了,LocalDateTime是可以转成ZonedDateTime的。

推荐相关文章

hutool日期时间系列文章

1DateUtil(时间工具类)-当前时间和当前时间戳

2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换

3DateUtil(时间工具类)-获取日期的各种内容

4DateUtil(时间工具类)-格式化时间

5DateUtil(时间工具类)-解析被格式化的时间

6DateUtil(时间工具类)-时间偏移量获取

7DateUtil(时间工具类)-日期计算

8ChineseDate(农历日期工具类)

9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)

10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装

其他

要探索JDK的核心底层源码,那必须掌握native用法

万字博文教你搞懂java源码的日期和时间相关用法

java的SimpleDateFormat线程不安全出问题了,虚竹教你多种解决方案

源码分析:JDK获取默认时区的风险和最佳实践

本文转载自: 掘金

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

DDD战术设计实践 DDD概览 DDD推荐的架构模式 充血模

发表于 2021-11-02

在笔者学习 DDD 的过程中,大部分文章通常都是在谈 DDD 的概念,理论,诚然这些很重要,但 DDD 的读者大多还是习惯与传统开发的方式,而 DDD 的思想与传统开发模式大为不同,当大量的理论铺面而来的时候,难免觉得无从着力,本系列文章希望通过一个实际系统的 DDD 案例,让读者对 DDD 的落地有一定的认识,认识的同时也会产生新的疑问,带着这些疑问在回头去学习 DDD 的系统理论,相信能够对读者起到帮助。

DDD概览

此章节希望读者对DDD有一些基本概念,在本章中不会深入到具体概念的细节,在《实现领域驱动设计》一书中DDD每个概念背后都有一套详细的设计原则,后续文章中我们将结合编码的同时将一些概念与读者一起描述。

什么是领域驱动设计?

领域驱动设计目前被大量的提及,那么什么是领域驱动设计呢?笔者在刚开始接触时被这个问题纠结了很久,随着持续的学习,搜索大家对DDD的总结,发现DDD很难用一句话简单的描述清楚,让读者可以理解其含义。因此关于这个问题的解释我们就稍微繁琐一点,在领域驱动设计中,领域可以理解为业务,领域专家就是对业务很了解的人,比如你想要做一个在线车票的售票系统,那么平时我们看到的售票员可能就是领域专家,在比如你已经在一个业务上做了5年研发了,经历了各种需求的迭代,讨论,你懂得比新来的产品,业务还多,那么你有可能就是你们公司的领域专家。领域驱动设计的核心就是与领域专家一起通过领域建模的方式去设计的我们的软件程序。

  • 那么领域如何驱动设计?或者说业务如何驱动软件设计?

单纯聊这个问题很奇怪,我们平时开发不都是业务驱动的吗?是的,但仔细的琢磨一下我们的开发过程,你会发现其中的问题。我们在和业务(领域)专家讨论时,我们是想着将需求如何映射到代码上,还是想着应该创建那些表,改那些表字段才能满足需求呢?我们在拿到一个产品原型,需求清单第一步是写代码还是创建数据表呢?大多数时候答案是后者,因此我们实际是将面向业务开发转换为了面向数据开发。

那么DDD如何解决这个问题呢,答案是领域模型,我个人认为领域模型的核心是通过模型承载和保存领域知识,并通过模型与代码的映射将这些领域知识保存在程序代码中。在传统的开发中,当业务被转换为一张张数据表时,丢失最多的就是领域知识。

DDD可以做什么

DDD主要分为两个部分,战略设计与战术设计,战略设计围绕微服务拆分,战术设计围绕微服务构建

DDD怎么做

  1. 领域专家与研发人员一起(研发人员可能就是领域专家),通过一系列的方式方法(DDD并没有明确说明用什么方法),划分出业务的边界,这个边界就是限界上下文,微服务可以以限界上下文指定微服务的拆分,但是微服务的拆分并不是说一定以限界上下文为边界,这里面还需要考虑其它因数,比如3个火枪手原则、两个披萨原则以及组织架构对微服务拆分的影响等。
  2. 研发人员通过领域模型,领域模型就是DDD中用于指定微服务实现的模型,保存领域知识,通过这种方式DDD通过领域模型围绕业务进⾏建模,并将模型与代码进⾏映射,业务调整影响代码的同时,代码也能直接的反映业务。

按照常规的编码⽅式,代码就不能直接反映业务了吗? 请参考贫血模型与充血模型

充血模型编码实践

DDD领域模型

实体与值对象

  • 实体的特征
  1. 唯一标识,对唯一性事物进行建模
  2. 包含了业务的关键行为,可以随着业务持续变化
  3. 修改时,因为有唯一标识,所以还是同一个实体

在上图中,订单就是一个实体,因为他有订单的唯一ID,通过它可以表示订单这个事务的唯一性,并且在订单的整个生命周期,随着业务订单也在不断的变化,创建订单到订单完成,订单状态在不断的变化,但是因为它们有唯一的订单ID,所以它们就是同一个实体。

  • 值对象的特征
  1. 描述事物的某个特征,通常作为实体属性存在
  2. 创建后即不可变
  3. 修改时,用另一个值对象予以替换

在上图中,订单商品就是一个值对象,因为在订单语境下,商品就是订单的一个特征,同时订单中的商品在订单创建的那一刻就会被”快照”下来,如果商品的发生变化,比如价格从100元涨价到10000元,订单中的商品也不会同步去修改。
在此种业务语境下,订单商品就符合对值对象的描述,那么如果卖家修改订单中商品的价格怎么办呢,在DDD中通过覆盖的方式进行修改,而不是只修改一个价格属性。

除了订单商品外,收获地址也是一个值对象,那么收获地址可以是一个实体吗? 答案是可以的,当业务在收获地址管理的上下文语境里的时候,收获地址就是一个实体。

更多对实体特征的描述,可以参考《实现领域驱动设计》一书

领域服务

领域服务可以帮助我们分担实体的功能,承接部分业务逻辑,做一些实体不变处理的业务流程,它不是必须的。
在上图中,描述的是一个创建消息的领域服务,因为消息的实体中有用户的值对象,但是用户的信息通常在另一个限界上下文,也就是另一个微服务中,因此需要通过一些facade接口获取,如果把这些接口的调用防在领域实体
中就会导致实体过于臃肿,且也不必保持其独立性,因为它需要被类似于Spring这样的框架进行管理,依赖注入一些接口,因此通过领域服务进行辅助是一种很好的方式。

聚合

将实体和值对象在一致性边界之内组成聚合,使用聚合划分限界上下文(微服务)内部的边界,聚合根做为一种特殊的实体,用于管理聚合内部的实体与值对象,并将自身暴露给外部进行引用。

比如在上图中描述的是一个订单聚合,在这个聚合中,它里面有两个实体,一个是订单一个是退货退款协议,显然退货退款协议应该依托于订单,但是它也符合实体的特征,因此被定义为实体。在此情况下,订单实体就是此聚合的聚合根。

聚合的一致性边界

生命周期一致性

生命周期的一致性,聚合对外的生命周期保持一致,聚合根生命周期结束,聚合的内部所有对象的生命周期也都应该结束。

事务的一致性

事务的一致性,这里的事务指的是数据库事务,每个数据库事务指包含一个聚合,不应该有垮聚合的事务。

领域事件

领域事件表示领域中所发生的事情,通过领域事件可以实现微服务内的信息同步,同时也可以实现对外部系统的解耦。

如上图所示,聚合变更后创建领域事件,领域事件有两种方式进行发布。

  1. 与聚合事务一起进行存储,比如存储进一个本地事件表,在由事件转发器转发到消息队列,这样保证的事件不会丢失。
  2. 直接进行转发到消息队列,但是此时因为事件还未入口,因此需要在聚合事务与消息队列发布事件之间做XA的2PC事务提交,因为有2PC存在,通常性能不会太好。

除了向外部系统发布事件,限界上下文内部的多个聚合也可以通过一些本地事务发布器来进行事务的发布,比如Spring Event 或 EventBus等

资源库

资源库是保存聚合的地方,将聚合实例存放在资源库(Repository)中,之后再通过该资源库来获取相同的实例。

  1. Save: 聚合对象由Repository的实现,转换为存储所支持的数据结构进行持久化
  2. Find: 根据存储所支持的数据结构,由Repository的实现转换为聚合对象

应用服务

应用服务负责流程编排,它将要实现的功能委托给一个或多个领域对象来实现,本身只负责处理业务用例的执行顺序以及结果的拼装同时也可以在应用服务做些权限验证等工作。

DDD推荐的架构模式

本章我们来聊一聊DDD推荐的架构模式,这些架构模式用于指导服务内的具体实现,对于服务内的逻辑分层,职能角色,依赖关系都有现实的指导意义。

DDD分层

在一个典型的DDD分层架构中,分为用户界面层(Interfacce) , 应用层(Application), 领域层(Domain) ,基础设施层 (Infrastructure), 其中领域层是DDD分层架构中的核心,它是保存领域知识的地方。

分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合。

在传统的DDD分层中,下图是他们的依赖关系。

如果读者没有使用过DDD可能对此理解不是很直观,可以将用户界面层想象为Controller,应用层与领域层想象为Service,基础设施层想象为Repository或者DAO,可能会好理解一些

可以看到,在传统的DDD分层架构中,基础层是被其它层所共同依赖的,它处于最底层,这可能导致重心偏移(想象一下在Service依赖DAO的场景),然而在DDD中领域层才是核心,因此要改变这种依赖。

如何改变这种依赖关系呢,在面向对象设计中有一种设计原则叫做依赖导致原则( Dependence Inversion Principle,DIP)。

DIP的定义为:

高层模块不应该依赖于底层模块,二者都应该依赖于抽象。

抽象不应该依赖于细节,细节应该依赖于抽象。

根据DIP改进以后的架构如下图所示。

改进后的DDD分层,将整个依赖过程反过来了,但实际上仅仅是反过来了这么简单吗?在DIP的理论中,高层模块与低层模块是不相互依赖的,他们都依赖于一个抽象,那么这么看来,模块之间就不在是一种强耦合的关系了。

比如,在DIP之前,领域层之间依赖于基础设施层。

改进后,他们后依赖于IUserRepository的抽象,抽象由基础层去实现,领域层并不关心如何实现。

由此各模块可以对内实现强内聚对外提供松耦合依赖。

六边形架构(端口适配器架构)

六边形架构,对于每种外界类型,都有一个适配器与之相对应。业务核心逻辑被包裹在内部,外界通过应用层API与内部进行交互,内部的实现无须关注外部的变化,更加聚焦。在这种架构下还可以轻易地开发用于测试的适配器。
同时六边形架构又名“端口适配器架构”, 这里的端口不一定指传统意义上的服务端口,可以理解为一种通讯方式,比如在一个服务中,我们可能会提供给用户浏览器的基于HTTP的通讯方式,提供给服务内部的基于RPC的通讯方式,以及基于MQ的通讯方式等,适配器指的是用于将端口输入转换为服务内部接口可以理解的输入。

刚才我们讨论的是外部向领域服务内部输入部分的端口+适配器模式,同时输出时也同样,比如当我们的要将领域对象进行存储时,我们知道有各种各样的存储系统,比如Mysql、ES、Mongo等,假如说我们可以抽象出一个适配器,用于适配不同的存储系统,那么我们就可以灵活的切换不同的存储介质,这对于我们开发测试,以及重构都是很有帮助的,而在DDD中这个抽象的适配器就资源库。

理解到这些以后,我们来看下六边形架构的整体架构。

在此中架构下,业务层被聚焦在内部的六边形,内部的六边形不关心外部如何运作,只关注与内部的业务实现,这也是DDD推崇的方式,研发人员应该更关注于业务的实现也就是领域层的工作,而不是
聚焦在技术的实现。结合分层架构的思想,外部的六边形可以理解为接口层与基础层,内部理解为应用层与领域层,内部通过DIP与外部解耦。

在《实现领域驱动设计》一书中,作者认为它是一种具有持久生命力的架构。

​

充血模型编码实践

本章我们将对通过《重构》一书中的案例,回顾贫血模型与充血模型,为后面的编码做知识储备,在DDD实践中,我们将大量用到充血模型的编码方式,如果你对贫血模型与充血模型已经了解了,可以跳过本章。

什么是贫血模型与充血模型?

回答这个问题,我们从《重构》一书中的一个影片租赁的案例,以及一个订单的开发场景,分别使用贫血模型与充血模型来实现,读者可以从中感受其差别理解它们的不同。

影片租赁场景

需要说明的是下面的代码基本与《重构》一书中的代码相同,但笔者省略了重构的各个代码优化环节,只展示了贫血模型与充血模型代码的不同。书中源代码,笔者也手写了一份实现,感兴趣可以通过以下链接点击查看。

gitee.com/izhengyin/s…

需求描述

根据顾客租聘的影片打印出顾客消费金额与积分

  • 积分规则
    • 默认租聘积一分,如果是新片且租聘大于1天,在加一分
  • 费用规则
    • 普通片 ,租聘起始价2元,如果租聘时间大于2天,每天增加1.5元
    • 新片 ,租聘价格等于租聘的天数
    • 儿童片 ,租聘起始价1.5元,如果租聘时间大于3天,每天增加1.5元

基于贫血模型的实现

下面是影片 Movie 、租赁 Rental 两个贫血模型类,下面这样的代码在我们日常开发中是比较常见,简单来说它们就是只包含数据,不包含业务逻辑的类,从面向对象角度来说也违背了面向对象里面封装的设计原则。

面向对象封装:隐藏信息、保护数据,只暴露少量接口,提高代码的可维护性与易用性;

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
arduino复制代码public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private Integer priceCode;

public Movie(String title, Integer priceCode) {
this.title = title;
this.priceCode = priceCode;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Integer getPriceCode() {
return priceCode;
}

public void setPriceCode(Integer priceCode) {
this.priceCode = priceCode;
}
}
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
csharp复制代码public class Rental {
/**
* 租的电影
*/
private Movie movie;
/**
* 已租天数
*/
private int daysRented;

public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}

public Movie getMovie() {
return movie;
}

public void setMovie(Movie movie) {
this.movie = movie;
}

public int getDaysRented() {
return daysRented;
}

public void setDaysRented(int daysRented) {
this.daysRented = daysRented;
}
}

接着是我们的Customer类,Customer类的问题是里面包含了原本应该是Movie与Reatal的业务逻辑,给人感觉很重,Customer可以类别我们日常开发的XxxService,想想我们是不是在Service层中不断的堆砌业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
arduino复制代码public class Customer {
private String name;
private List<Rental> rentals = new ArrayList<>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
this.rentals.add(rental);
}
public String getName() {
return name;
}

/**
* 根据顾客租聘的影片打印出顾客消费金额与积分
* @return
*/
public String statement(){
double totalAmount = 0;
String result = getName()+"的租聘记录 \n";
for (Rental each : rentals){
double thisAmount = getAmount(each);
result += "\t" + each.getMovie().getTitle() + " \t" + thisAmount +" \n";
totalAmount += thisAmount;
}
int frequentRenterPoints = getFrequentRenterPoints(rentals);
result += "租聘总价 : "+ totalAmount + "\n";
result += "获得积分 : "+ frequentRenterPoints;
return result;
}

/**
* 获取积分总额
* @param rentals
* @return
*/
private int getFrequentRenterPoints(List<Rental> rentals){
return rentals.stream()
.mapToInt(rental -> {
//默认租聘积一分,如果是 Movie.NEW_RELEASE 且租聘大于1天,在加一分
int point = 1;
if(rental.getMovie().getPriceCode().equals(Movie.NEW_RELEASE) && rental.getDaysRented() > 1){
point ++;
}
return point;
})
.sum();
}

/**
* 获取单个影片租聘的价格
* 1. 普通片 ,租聘起始价2元,如果租聘时间大于2天,每天增加1.5元
* 2. 新片 ,租聘价格等于租聘的天数
* 3. 儿童片 ,租聘起始价1.5元,如果租聘时间大于3天,每天增加1.5元
* @param rental
* @return
*/
private double getAmount(Rental rental){
double thisAmount = 0;
switch (rental.getMovie().getPriceCode()){
case Movie.REGULAR:
thisAmount += 2;
if(rental.getDaysRented() > 2){
thisAmount += (rental.getDaysRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += rental.getDaysRented();
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if(rental.getDaysRented() > 3){
thisAmount += (rental.getDaysRented() - 3) * 1.5;
}
break;
default:
//nothings todo
break;
}
return thisAmount;
}

}

最后我们运行主程序类,进行输出,得到下面结果,记住这个结果,我们会通过重新模型重构后,保证同样的输出。

1
2
3
4
5
6
markdown复制代码张三的租聘记录 
儿童片 1.5
普通片 3.5
新片 5.0
租聘总价 : 10.0
获得积分 : 4

主程序类

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Main {
public static void main(String[] args) {
Movie movie1 = new Movie("儿童片", Movie.CHILDRENS);
Movie movie2 = new Movie("普通片", Movie.REGULAR);
Movie movie3 = new Movie("新片", Movie.NEW_RELEASE);
Customer customer = new Customer("张三");
customer.addRental(new Rental(movie1,1));
customer.addRental(new Rental(movie2,3));
customer.addRental(new Rental(movie3,5));
System.out.println(customer.statement())
}
}

基于充血模型的实现

我们的类没有变化,只是类里面的实现发生了变化,接下来就逐一看看类的实现都发生了那些改变。

重构后影片 Movie 类

  1. 删除了不必要setXXX方法
  2. 增加了 getCharge 获取费用电影费用的方法,将原本 Customer 的逻辑交由Movie类实现。

注:Movie类还有优化空间,但不是本文的重点,读者感兴趣可以查看此链接
gitee.com/izhengyin/s…

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
arduino复制代码public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private Integer priceCode;

public Movie(String title, Integer priceCode) {
this.title = title;
this.priceCode = priceCode;
}

public String getTitle() {
return title;
}

public Integer getPriceCode() {
return priceCode;
}

/**
*获取单个影片租聘的价格
* 1. 普通片 ,租聘起始价2元,如果租聘时间大于2天,每天增加1.5元
* 2. 新片 ,租聘价格等于租聘的天数
* 3. 儿童片 ,租聘起始价1.5元,如果租聘时间大于3天,每天增加1.5元
* @param daysRented
* @return
*/
public double getCharge(int daysRented){
double thisAmount = 0;
switch (this.priceCode){
case REGULAR:
thisAmount += 2;
if(daysRented > 2){
thisAmount += (daysRented - 2) * 1.5;
}
break;
case NEW_RELEASE:
thisAmount += daysRented;
break;
case CHILDRENS:
thisAmount += 1.5;
if(daysRented > 3){
thisAmount += (daysRented - 3) * 1.5;
}
break;
default:
//nothings todo
break;
}
return thisAmount;
}
}

重构后租赁 Rental 类

  1. 移除了部分不必要的 get / set 方法
  2. 增加一个 getPoint 方法,计算租赁积分,将原本 Customer 的逻辑交由获取积分的业务交由getPoint实现,但总积分的计算还是在Customer。
  3. 增加一个 getCharge 方法,具体调用Movie::getCharge传入租赁天数得到租赁的费用,因为在这个需求中主体是租赁
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
csharp复制代码public class Rental {

/**
* 租的电影
*/
private Movie movie;

/**
* 已租天数
*/
private int daysRented;

public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}

public Movie getMovie() {
return movie;
}

/**
* 默认租聘积一分,如果是新片且租聘大于1天,在加一分
* @return
*/
public int getPoint(){
int point = 1;
if(this.movie.getPriceCode().equals(Movie.NEW_RELEASE) && this.daysRented > 1){
point ++;
}
return point;
}
/**
* 获取费用
* @return
*/
public double getCharge(){
return this.movie.getCharge(this.daysRented);
}
}

瘦身后的Customer

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
arduino复制代码public class Customer {
private String name;
private List<Rental> rentals = new ArrayList<>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
this.rentals.add(rental);
}
public String getName() {
return name;
}

/**
* 根据顾客租聘的影片打印出顾客消费金额与积分
* @return
*/
public String statement(){
double totalAmount = 0;
String result = getName()+"的租聘记录 \n";
for (Rental each : rentals){
double thisAmount = each.getCharge();
result += "\t" + each.getMovie().getTitle() + " \t" + thisAmount +" \n";
totalAmount += thisAmount;
}
int frequentRenterPoints = getFrequentRenterPoints(rentals);
result += "租聘总价 : "+ totalAmount + "\n";
result += "获得积分 : "+ frequentRenterPoints;
return result;
}

/**
* 获取积分总额
* @param rentals
* @return
*/
private int getFrequentRenterPoints(List<Rental> rentals){
return rentals.stream()
.mapToInt(Rental::getPoint)
.sum();
}
}

最后我们运行主程序类,得到同样的输出。

源码地址: gitee.com/izhengyin/d…

订单的场景

需求描述

  1. 创建订单
  2. 设置订单优惠

订单场景贫血模型实现

Order 类 , 只包含了属性的Getter,Setter方法

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码@Data
public class Order {
private long orderId;
private int buyerId;
private int sellerId;
private BigDecimal amount;
private BigDecimal shippingFee;
private BigDecimal discountAmount;
private BigDecimal payAmount;
private String address;
}

OrderService ,根据订单创建中的业务逻辑,组装order数据对象,最后进行持久化

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
scss复制代码    /**
* 创建订单
* @param buyerId
* @param sellerId
* @param orderItems
*/
public void createOrder(int buyerId,int sellerId,List<OrderItem> orderItems){
//新建一个Order数据对象
Order order = new Order();
order.setOrderId(1L);
//算订单总金额
BigDecimal amount = orderItems.stream()
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO,BigDecimal::add);
order.setAmount(amount);
//运费
order.setShippingFee(BigDecimal.TEN);
//优惠金额
order.setDiscountAmount(BigDecimal.ZERO);
//支付总额 = 订单总额 + 运费 - 优惠金额
BigDecimal payAmount = order.getAmount().add(order.getShippingFee()).subtract(order.getDiscountAmount());
order.setPayAmount(payAmount);
//设置买卖家
order.setBuyerId(buyerId);
order.setSellerId(sellerId);
//设置收获地址
order.setAddress(JSON.toJSONString(new Address()));
//写库
orderDao.insert(order);
orderItems.forEach(orderItemDao::insert);
}

在此种方式下,核心业务逻辑散落在OrderService中,比如获取订单总额与订单可支付金额是非常重要的业务逻辑,同时对象数据逻辑一同混编,在此种模式下,代码不能够直接反映业务,也违背了面向对象的SRP原则。

设置优惠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码 /**
* 设置优惠
* @param orderId
* @param discountAmount
*/
public void setDiscount(long orderId, BigDecimal discountAmount){
Order order = orderDao.find(orderId);
order.setDiscountAmount(discountAmount);
//从新计算支付金额
BigDecimal payAmount = order.getAmount().add(order.getShippingFee()).subtract(discountAmount);
order.setPayAmount(payAmount);
//orderDao => 通过主键更新订单信息
orderDao.updateByPrimaryKey(order);
}

贫血模型在设置折扣时因为需要考虑到折扣引发的支付总额的变化,因此还需要在从新的有意识的计算支付总额,因为面向数据开发需要时刻考虑数据的联动关系,在这种模式下忘记了修改某项关联数据的情况可能是时有发生的。

订单场景充血模型实现

Order 类,包含了业务关键属于以及行为,同时具有良好的封装性

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
csharp复制代码/**
* @author zhengyin
* Created on 2021/10/18
*/
@Getter
public class Order {
private long orderId;
private int buyerId;
private int sellerId;
private BigDecimal shippingFee;
private BigDecimal discountAmount;
private Address address;
private Set<OrderItem> orderItems;

//空构造,只是为了方便演示
public Order(){}

public Order(long orderId,int buyerId ,int sellerId,Address address, Set<OrderItem> orderItems){
this.orderId = orderId;
this.buyerId = buyerId;
this.sellerId = sellerId;
this.address = address;
this.orderItems = orderItems;
}

/**
* 更新收货地址
* @param address
*/
public void updateAddress(Address address){
this.address = address;
}
/**
* 支付总额等于订单总额 + 运费 - 优惠金额
* @return
*/
public BigDecimal getPayAmount(){
BigDecimal amount = getAmount();
BigDecimal payAmount = amount.add(shippingFee);
if(Objects.nonNull(this.discountAmount)){
payAmount = payAmount.subtract(discountAmount);
}
return payAmount;
}

/**
* 订单总价 = 订单商品的价格之和
* amount 可否设置为一个实体属性?
*/
public BigDecimal getAmount(){
return orderItems.stream()
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO,BigDecimal::add);
}


/**
* 运费不能为负
* @param shippingFee
*/
public void setShippingFee(BigDecimal shippingFee){
Preconditions.checkArgument(shippingFee.compareTo(BigDecimal.ZERO) >= 0, "运费不能为负");
this.shippingFee = shippingFee;
}

/**
* 设置优惠
* @param discountAmount
*/
public void setDiscount(BigDecimal discountAmount){
Preconditions.checkArgument(discountAmount.compareTo(BigDecimal.ZERO) >= 0, "折扣金额不能为负");
this.discountAmount = discountAmount;
}

/**
* 原则上,返回给外部的引用,都应该防止间接被修改
* @return
*/
public Set<OrderItem> getOrderItems() {
return Collections.unmodifiableSet(orderItems);
}
}

OrderService , 仅仅负责流程的调度

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    /**
* 创建订单
* @param buyerId
* @param sellerId
* @param orderItems
*/
public void createOrder(int buyerId, int sellerId, Set<OrderItem> orderItems){
Order order = new Order(1L,buyerId,sellerId,new Address(),orderItems);
//运费不随订单其它信息一同构造,因为运费可能在后期会进行修改,因此提供一个设置运费的方法
order.setShippingFee(BigDecimal.TEN);
orderRepository.save(order);
}

在此种模式下,Order类完成了业务逻辑的封装,OrderService仅负责业务逻辑与存储之间的流程编排,并不参与任何的业务逻辑,各模块间职责更明确。

设置优惠

1
2
3
4
5
6
7
8
9
10
11
scss复制代码
/**
* 设置优惠
* @param orderId
* @param discountAmount
*/
public void setDiscount(long orderId, BigDecimal discountAmount){
Order order = orderRepository.find(orderId);
order.setDiscount(discountAmount);
orderRepository.save(order);
}

在充血模型的模式下,只需设置具体的优惠金额,因为在Order类中已经封装了相关的计算逻辑,比如获取支付总额时,是实时通过优惠金额来计算的。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码   /**
* 支付总额等于订单总额 + 运费 - 优惠金额
* @return
*/
public BigDecimal getPayAmount(){
BigDecimal amount = getAmount();
BigDecimal payAmount = amount.add(shippingFee);
if(Objects.nonNull(this.discountAmount)){
payAmount = payAmount.subtract(discountAmount);
}
return payAmount;
}

写到这里,可能读者会有疑问,文章都在讲充血模型的业务,那数据怎么进行持久化?

数据持久化时我们通过封装的 OrderRepository 来进行持久化操作,根据存储方式的不同提供不同的实现,以数据库举例,那么我们需要将Order转换为PO对象,也就是持久化对象,这时的持久化对象就是面向数据表的贫血模型对象。

比如下面的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码public class OrderRepository {
private final OrderDao orderDao;
private final OrderItemDao orderItemDao;


public OrderRepository(OrderDao orderDao, OrderItemDao orderItemDao) {
this.orderDao = orderDao;
this.orderItemDao = orderItemDao;
}

public void save(Order order){
// 在此处通过Order实体,创建数据对象 new OrderPO() ; new OrderItemPO();
// orderDao => 存储订单数据
// orderItemDao => 存储订单商品数据

}

public Order find(long orderId){
//找到数据对象,OrderPO
//找到数据对象,OrderItemPO
//组合返回,order实体
return new Order();
}
}

通过上面两种实现方式的对比,相信读者对两种模型已经有了明确的认识了,在贫血模型中,数据和业务逻辑是割裂的,而在充血模型中数据和业务是内聚的。

电商消息系统编码实践

编码实践部分需要涉及大量的源码信息,请根据下面的链接访问源码进行查阅

DDD电商消息系统编码实践(一)

DDD电商消息系统编码实践(二)

DDD电商消息系统编码实践(三)

DDD电商消息系统编码实践(四)

DDD电商消息系统编码实践(五)

本文源码地址:gitee.com/izhengyin/d…

参考:

  • 《重构,改善既有代码的设计》马丁·福勒(Martin Fowler)
  • 《领域驱动设计:软件核心复杂性应对之道》(修订版)埃里克 埃文斯(Eric Evans)
  • 《实现领域驱动设计》Vaughn.Vernon(沃恩.弗农)
  • 《架构整洁之道》Robert C. Martin(罗伯特C.马丁)
  • 《DDD 实战课》极客时间 , 欧创新
  • Thoughworks 洞见技术博客,DDD 专栏 insights.thoughtworks.cn/tag/domain-…
  • 微软 Azure 技术博客,云原生架构设计模式专栏 docs.microsoft.com/en-us/azure…
  • github.com/citerus/ddd…​
  • github.com/e-commerce-…​
  • github.com/ouchuangxin…

​

​

本文转载自: 掘金

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

Go 复合数据类型slice slice

发表于 2021-11-02

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

slice

slice 表示用于相同类型元素的可变长度的序列。

slice有三个属性:指针、长度和容量。

  • 指针:slice存储数据的内部结构是数组,指针指向的是数组的地址
  • 长度:保存slice中的元素数量
  • 容量:slice中可容纳的元素数量,在像slice插入元素时,如果超过容量,会对slice进行扩容

slice的基本操作

1.初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// 方式一 通过字面量自己初始化
s := []int{}
months := []string{1: "January", /* ... */, 12: "December"}
var z []int

// 方式二 通过切片语法从数组或slice中生成
a := [10]int{1, 2, 3}
b := a[1: 3]
c := b[1:]

// 注意这种奇怪的方式,可以指定具体某个下标的值
e := []int{1:42, 55, 66, 77, 7:88}
fmt.Println(e) // [0 42 55 66 77 0 0 88]

2.访问和更改

1
2
3
4
5
6
7
8
9
10
go复制代码// 访问和数组一致,通过下标访问
a := []int{1, 2, 3}
fmt.Println(a[1]) // 2

// 更改
a[1] = 100
fmt.Println(a[1]) // 100
b := a[:]
b[1] = 101 // b和a指向的底层数据一样的,所以b更改会影响a
fmt.Println(b[1], a[1]) // 101 101

3.增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// 如果想对slice增加一个元素,使用append函数
a := []int{1, 2, 3}
a = append(a, 4)
fmt.Println(a) // [1 2 3 4]
// 使用len函数获取当前slice中元素数量
// 使用cap函数获取当前slice所能支撑的容颜
for i := 5; i < 15; i++ {
a = append(a, i)
fmt.Printf("len: %d\t cap: %d\n", len(a), cap(a))
}
/*OUTPUT:
[1 2 3 4]
len: 5 cap: 6
len: 6 cap: 6
len: 7 cap: 12
len: 8 cap: 12
len: 9 cap: 12
len: 10 cap: 12
len: 11 cap: 12
len: 12 cap: 12
len: 13 cap: 24
len: 14 cap: 24
*/

append函数会处理当当前slice对象的容量不足时,自动扩容(重新申请一块空间并将原来的元素复制过来)。我无法保证原始的slice和调用append后的结果slice执向同一个底层数组。也无法断定旧slice上对元素的操作会或者不会影响新的slice元素。 所以通常我们将append的调用结果再次赋值给传入append函数的slice。

4.删除

1
2
3
4
5
6
7
8
9
go复制代码// 通过copy实现删除
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
a := []{1, 2, 3}

a = remove(a, 1)
fmt.Println(a) // [1 3]

5.翻转slice

1
2
3
4
5
6
7
8
9
go复制代码func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

a := []int{1, 2, 3}
reverse(a)
fmt.Println(a) // [3 2 1]

6.比较

slice无法使用==进行比较,只允许和nil进行比较。如果想检查slice是否为空,使用len(s) == 0。

1
2
3
4
go复制代码var s []int     // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil

slice的用途

1.实现栈

栈的特点是先进后出

1
2
3
4
5
6
go复制代码stack = []int{}
// 入栈
stack = append(stack, 1)
// 出栈
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]

本文转载自: 掘金

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

90%的人以为会用ThreadPoolExecutor了,看

发表于 2021-11-02

在阿里巴巴手册中有一条建议:

【强制】线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

如果经常基于Executors提供的工厂方法创建线程池,很容易忽略线程池内部的实现。特别是拒绝策略,因使用Executors创建线程池时不会传入这个参数,直接采用默认值,所以常常被忽略。

下面我们就来了解一下线程池相关的实现原理、API以及实例。

线程池的作用

在实践应用中创建线程池主要是为了:

  • 减少资源开销:减少每次创建、销毁线程的开销;
  • 提高响应速度:请求到来时,线程已创建好,可直接执行,提高响应速度;
  • 提高线程的可管理性:线程是稀缺资源,需根据情况加以限制,确保系统稳定运行;

ThreadPoolExecutor

ThreadPoolExecutor可以实现线程池的创建。ThreadPoolExecutor相关类图如下:

image-20211030204753631

从类图可以看出,ThreadPoolExecutor最终实现了Executor接口,是线程池创建的真正实现者。

Executor两级调度模型

image-20211030205542375

在HotSpot虚拟机中,Java中的线程将会被一一映射为操作系统的线程。在Java虚拟机层面,用户将多个任务提交给Executor框架,Executor负责分配线程执行它们;在操作系统层面,操作系统再将这些线程分配给处理器执行。

ThreadPoolExecutor的三个角色

任务

ThreadPoolExecutor接受两种类型的任务:Callable和Runnable。

  • Callable:该类任务有返回结果,可以抛出异常。通过submit方法提交,返回Future对象。通过get获取执行结果。
  • Runnable:该类任务只执行,无法获取返回结果,在执行过程中无法抛异常。通过execute或submit方法提交。

任务执行器

Executor框架最核心的接口是Executor,它表示任务的执行器。

通过上面类图可以看出,Executor的子接口为ExecutorService。再往底层有两大实现类:ThreadPoolExecutor和ScheduledThreadPoolExecutor(集成自ThreadPoolExecutor)。

执行结果

Future接口表示异步的执行结果,它的实现类为FutureTask。

三个角色之间的处理逻辑图如下:

image-20211030211254131

线程池处理流程

image-20211030215025030

一个线程从被提交(submit)到执行共经历以下流程:

  • 线程池判断核心线程池里是的线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下一个流程;
  • 线程池判断工作队列是否已满。如果工作队列没有满,则将新提交的任务储存在这个工作队列里。如果工作队列满了,则进入下一个流程;
  • 线程池判断其内部线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已满了,则交给饱和策略来处理这个任务。

线程池在执行execute方法时,主要有以下四种情况:

image-20211030215245520

  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获得全局锁);
  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue;
  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获得全局锁);
  • 如果创建新线程将使当前运行的线程超出maxiumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

线程池采取上述的流程进行设计是为了减少获取全局锁的次数。在线程池完成预热(当前运行的线程数大于或等于corePoolSize)之后,几乎所有的excute方法调用都执行步骤二。

线程的状态流转

顺便再回顾一下线程的状态的转换,在JDK中Thread类中提供了一个枚举类,例举了线程的各个状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vbnet复制代码    public enum State {
​
      NEW,
​
      RUNNABLE,
​
      BLOCKED,
​
      WAITING,
​
      TIMED_WAITING,
​
      TERMINATED;
  }

一共定义了6个枚举值,其实代表的是5种类型的线程状态:

  • NEW:新建;
  • RUNNABLE:运行状态;
  • BLOCKED:阻塞状态;
  • WAITING:等待状态,WAITING和TIMED_WAITING可以归为一类,都属于等待状态,只是后者可以设置等待时间,即等待多久;
  • TERMINATED:终止状态;

线程关系转换图:

image-20211030221145296

当new Thread()说明这个线程处于NEW(新建状态);调用Thread.start()方法表示这个线程处于RUNNABLE(运行状态);

但是RUNNABLE状态中又包含了两种状态:READY(就绪状态)和RUNNING(运行中)。调用start()方法,线程不一定获得了CPU时间片,这时就处于READY,等待CPU时间片,当获得了CPU时间片,就处于RUNNING状态。

在运行中调用synchronized同步的代码块,没有获取到锁,这时会处于BLOCKED(阻塞状态),当重新获取到锁时,又会变为RUNNING状态。在代码执行的过程中可能会碰到Object.wait()等一些等待方法,线程的状态又会转变为WAITING(等待状态),等待被唤醒,当调用了Object.notifyAll()唤醒了之后线程执行完就会变为TERMINATED(终止状态)。

线程池的状态

线程池中状态通过2个二进制位(bit)来表示线程池的5个状态:RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED:

  • RUNNING:线程池正常工作的状态,在 RUNNING 状态下线程池接受新的任务并处理任务队列中的任务;
  • SHUTDOWN:调用shutdown()方法会进入 SHUTDOWN 状态。在 SHUTDOWN 状态下,线程池不接受新的任务,但是会继续执行任务队列中已有的任务;
  • STOP:调用shutdownNow()会进入 STOP 状态。在 STOP 状态下线程池既不接受新的任务,也不处理已经在队列中的任务。对于还在执行任务的工作线程,线程池会发起中断请求来中断正在执行的任务,同时会清空任务队列中还未被执行的任务;
  • TIDYING:当线程池中的所有执行任务的工作线程都已经终止,并且工作线程集合为空的时候,进入 TIDYING 状态;
  • TERMINATED:当线程池执行完terminated()钩子方法以后,线程池进入终态 TERMINATED;

ThreadPoolExecutor API

ThreadPoolExecutor创建线程池API:

1
2
3
4
5
6
7
java复制代码public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)

参数解释:

  • corePoolSize :线程池常驻核心线程数。创建线程池时,线程池中并没有任何线程,当有任务来时才去创建线程,执行任务。提交一个任务,创建一个线程,直到需要执行的任务数大于线程池基本大小,则不再创建。当创建的线程数等于corePoolSize 时,会加入设置的阻塞队列。
  • maximumPoolSize :线程池允许创建的最大线程数。当队列满时,会创建线程执行任务直到线程池中的数量等于maximumPoolSize。
  • keepAliveTime :当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
  • unit :keepAliveTime的时间单位,可选项:天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微妙)。
  • workQueue :用来储存等待执行任务的队列。
  • threadFactory :线程工厂,用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。
  • handler :执行拒绝策略对象。当达到任务缓存上限时(即超过workQueue参数能存储的任务数),执行拒接策略。也就是当任务处理不过来的时候,线程池开始执行拒绝策略。JDK 1.5提供了四种饱和策略:
+ AbortPolicy:默认,直接抛异常;
+ 只用调用者所在的线程执行任务,重试添加当前的任务,它会自动重复调用execute()方法;
+ DiscardOldestPolicy:丢弃任务队列中最久的任务;
+ DiscardPolicy:丢弃当前任务;

适当的阻塞队列

当创建的线程数等于corePoolSize,会将任务加入阻塞队列(BlockingQueue),维护着等待执行的Runnable对象。

阻塞队列通常有如下类型:

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。可以限定队列的长度,接收到任务时,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误。
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。这个队列在接收到任务时,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize。
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue: 一个使用优先级队列实现的无界阻塞队列。队列内元素必须实现Delayed接口,这就意味着传入的任务必须先实现Delayed接口。这个队列在接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
  • SynchronousQueue: 一个不存储元素的阻塞队列。这个队列在接收到任务时,会直接提交给线程处理,而不保留它,如果所有线程都在工作就新建一个线程来处理这个任务。所以为了保证不出现【线程数达到了maximumPoolSize而不能新建线程】的错误,使用这个类型队列时,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大。
  • LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

明确的拒绝策略

当任务处理不过来时,线程池开始执行拒绝策略。

支持的拒绝策略:

  • ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

线程池关闭

  • shutdown:将线程池状态置为SHUTDOWN,并不会立即停止。停止接收外部submit的任务,内部正在跑的任务和队列里等待的任务,会执行完后,才真正停止。
  • shutdownNow:将线程池状态置为STOP。企图立即停止,事实上不一定,跟shutdown()一样,先停止接收外部提交的任务,忽略队列里等待的任务,尝试将正在跑的任务interrupt中断(如果线程未处于sleep、wait、condition、定时锁状态,interrupt无法中断当前线程),返回未执行的任务列表。
  • awaitTermination(long timeOut, TimeUnit unit)当前线程阻塞,直到等所有已提交的任务(包括正在跑的和队列中等待的)执行完或者等超时时间到或者线程被中断,抛出InterruptedException,然后返回true(shutdown请求后所有任务执行完毕)或false(已超时)。

Executors

Executors是一个帮助类,提供了创建几种预配置线程池实例的方法:newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool等。

如果查看源码就会发现,Executors本质上就是实现了几类默认的ThreadPoolExecutor。而阿里巴巴开发手册,不建议采用Executors默认的,让使用者直接通过ThreadPoolExecutor来创建。

Executors.newSingleThreadExecutor()

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

1
arduino复制代码new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())

该类型线程池的结构图:

image-20211030213952177

该线程池的特点:

  • 只会创建一条工作线程处理任务;
  • 采用的阻塞队列为LinkedBlockingQueue;

Executors.newFixedThreadPool()

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

1
arduino复制代码new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

该类型线程池的结构图:

image-20211030213055780

该线程池的特点:

  • 固定大小;
  • corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
  • keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
  • 阻塞队列采用了LinkedBlockingQueue,一个无界队列;
  • 由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
  • 由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。

Executors.newCachedThreadPool()

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

1
vbnet复制代码new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

该类型线程池的结构图:

image-20211030213717232

该线程池的特点:

  • 可以无限扩大;
  • 比较适合处理执行时间比较小的任务;
  • corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
  • keepAliveTime为60S,意味着线程空闲时间超过60s就会被杀死;
  • 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理它,如果当前没有空闲的线程,那么就会再创建一条新的线程。

Executors.newScheduledThreadPool()

创建一个定长线程池,支持定时及周期性任务执行。

1
2
vbnet复制代码new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new DelayedWorkQueue());

该线程池类图:

image-20211030214309503

该线程池的特点:

  • 接收SchduledFutureTask类型的任务,有两种提交任务的方式:scheduledAtFixedRate和scheduledWithFixedDelay。SchduledFutureTask接收的参数:
+ time:任务开始的时间
+ sequenceNumber:任务的序号
+ period:任务执行的时间间隔
  • 采用DelayQueue存储等待的任务;
  • DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
  • DelayQueue也是一个无界队列;
  • 工作线程执行时,工作线程会从DelayQueue取已经到期的任务去执行;执行结束后重新设置任务的到期时间,再次放回DelayQueue;

Executors.newWorkStealingPool()

JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争。

1
2
3
4
5
csharp复制代码public static ExecutorService newWorkStealingPool() {
  return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
      ForkJoinPool.defaultForkJoinWorkerThreadFactory,
      null, true);
}

Executors方法的弊端

1)newFixedThreadPool 和 newSingleThreadExecutor:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)newCachedThreadPool 和 newScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

合理配置线程池大小

合理配置线程池,需要先分析任务特性,可以从以下角度来进行分析:

  • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  • 任务的优先级:高,中和低。
  • 任务的执行时间:长,中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

另外,还需要查看系统的内核数:

1
scss复制代码Runtime.getRuntime().availableProcessors());

根据任务所需要的CPU和IO资源可以分为:

  • CPU密集型任务: 主要是执行计算任务,响应时间很快,CPU一直在运行。一般公式:线程数 = CPU核数 + 1。只有在真正的多核CPU上才能得到加速,优点是不存在线程切换开销,提高了CPU的利用率并减少了线程切换的效能损耗。
  • IO密集型任务:主要是进行IO操作,CPU并不是一直在执行任务,IO操作(CPU空闲状态)的时间较长,应配置尽可能多的线程,其中的线程在IO操作时,其他线程可以继续利用CPU,从而提高CPU的利用率。一般公式:线程数 = CPU核数 * 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
typescript复制代码/**
* 任务实现线程
* @author sec
* @version 1.0
* @date 2021/10/30
**/
public class MyThread implements Runnable{
​
  private final Integer number;
​
  public MyThread(int number){
    this.number = number;
  }
​
  public Integer getNumber() {
    return number;
  }
​
  @Override
  public void run() {
    try {
        // 业务处理
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Hello! ThreadPoolExecutor - " + getNumber());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  }
}

自定义阻塞提交的ThreadLocalExcutor:

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
java复制代码/**
* 自定义阻塞提交的ThreadPoolExecutor
* @author sec
* @version 1.0
* @date 2021/10/30
**/
public class CustomBlockThreadPoolExecutor {
​
  private ThreadPoolExecutor pool = null;
​
  /**
  * 线程池初始化方法
  */
  public void init() {
    // 核心线程池大小
    int poolSize = 2;
    // 最大线程池大小
    int maxPoolSize = 4;
    // 线程池中超过corePoolSize数目的空闲线程最大存活时间:30+单位TimeUnit
    long keepAliveTime = 30L;
    // ArrayBlockingQueue<Runnable> 阻塞队列容量30
    int arrayBlockingQueueSize = 30;
    pool = new ThreadPoolExecutor(poolSize, maxPoolSize, keepAliveTime,
          TimeUnit.SECONDS, new ArrayBlockingQueue<>(arrayBlockingQueueSize), new CustomThreadFactory(),
          new CustomRejectedExecutionHandler());
  }
​
  /**
  * 关闭线程池方法
  */
  public void destroy() {
    if (pool != null) {
        pool.shutdownNow();
    }
  }
​
  public ExecutorService getCustomThreadPoolExecutor() {
    return this.pool;
  }
​
  /**
  * 自定义线程工厂类,
  * 生成的线程名词前缀、是否为守护线程以及优先级等
  */
  private static class CustomThreadFactory implements ThreadFactory {
​
    private final AtomicInteger count = new AtomicInteger(0);
​
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        String threadName = CustomBlockThreadPoolExecutor.class.getSimpleName() + count.addAndGet(1);
        t.setName(threadName);
        return t;
    }
  }
​
​
  /**
  * 自定义拒绝策略对象
  */
  private static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 核心改造点,将blockingqueue的offer改成put阻塞提交
        try {
          executor.getQueue().put(r);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
    }
  }
​
  /**
  * 当提交任务被拒绝时,进入拒绝机制,实现拒绝方法,把任务重新用阻塞提交方法put提交,实现阻塞提交任务功能,防止队列过大,OOM
  */
  public static void main(String[] args) {
​
    CustomBlockThreadPoolExecutor executor = new CustomBlockThreadPoolExecutor();
​
    // 初始化
    executor.init();
    ExecutorService pool = executor.getCustomThreadPoolExecutor();
    for (int i = 1; i < 51; i++) {
        MyThread myThread = new MyThread(i);
        System.out.println("提交第" + i + "个任务");
        pool.execute(myThread);
    }
​
    pool.shutdown();
    try {
        // 阻塞,超时时间到或者线程被中断
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
          // 立即关闭
          executor.destroy();
        }
    } catch (InterruptedException e) {
        executor.destroy();
    }
  }
}

小结

看似简单的线程池创建,其中却蕴含着各类知识,融合贯通,根据具体场景采用具体的参数进行设置才能够达到最优的效果。

总结一下就是:

  • 用ThreadPoolExecutor自定义线程池,要看线程的用途。如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM;
  • 如果任务量很大,且要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务;
  • 最大线程数一般设为2N+1最好,N是CPU核数;
  • 核心线程数,要根据任务是CPU密集型,还是IO密集型。同时,如果任务是一天跑一次,设置为0合适,因为跑完就停掉了;
  • 如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

参考文章:

[1]www.jianshu.com/p/94852bd1a…

[2]blog.csdn.net/jek123456/a…

[3]blog.csdn.net/z_s_z2016/a…

[4]zhuanlan.zhihu.com/p/33264000

[5]www.cnblogs.com/semi-sub/p/…

\

本文转载自: 掘金

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

Matplotlib更多实用图形的绘制

发表于 2021-11-02

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

前言

Matplotlib是Python的绘图库,它提供了一整套和 matlab 相似的命令 API,可以生成你所需的出版质量级别的图形。我们已经学习了一系列统计图来描绘两个变量间的基本关系,同时也学习了如何高度自定义统计的呈现样式,但是,仅仅使用这些图形并不足以应对所有场景。例如,我们需要可视化地显示降雨在各个地区的分布情况。 因此,我们需要更多的实用图形来表达现实世界的复杂关系。

可视化二维数组的内容

让我们从最简单的场景开始,假设我们有一个二维数组——著名的分形形状Mandelbrot,我们想将其可视化。
首先需要创建一个二维数组,然后调用plt.imshow()将其可视化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码import numpy as np
import matplotlib.cm as cm
from matplotlib import pyplot as plt
def iter_count(c, max_iter):
x = c
for n in range(max_iter):
if abs(x) > 2.:
return n
x = x ** 2 + c
return max_iter
n = 512
max_iter = 64
xmin, xmax, ymin, ymax = -2.2, .8, -1.5, 1.5
x = np.linspace(xmin, xmax, n)
y = np.linspace(ymin, ymax, n)
z = np.empty((n, n))
for i, y_i in enumerate(y):
for j, x_j in enumerate(x):
z[i, j] = iter_count(complex(x_j, y_i), max_iter)
plt.imshow(z, cmap = cm.Spectral)

可视化二维数组的内容

Tips:imshow()接受一个2D数组作为参数置,用于渲染图片,其中每个像素代表一个从2D数组中提取的值。像素的颜色从colormap中选取。2D数组中的数据也可以是自文件或其他源,例如我们完全可以将读取的图片绘制在图形中。

1
2
3
4
python复制代码# 读取图片
img = plt.imread('img.png')
# 绘制图片
plt.imshow(img)

图片的读取与绘制

我们也可以使用不同的颜色映射观察效果,只需要修改plt.imshow()可选参数cmap的值即可.

1
python复制代码plt.imshow(z, cmap = cm.binary, extent=(xmin, xmax, ymin, ymax))

修改颜色映射

Tips:plt.imshow()的可选参数extent指定存储在二维数组中的数据的坐标系——由四个值组成的元组,分别表示水平轴和垂直轴上的最小、最大范围。

接下来,将数组的尺寸由从512x512减少到32x32,看看效果如何:

1
python复制代码n = 64

缩小尺寸

Tips:使用32x32的数组表示Mandelbrot集时,得到的图片的尺寸并没有缩小,但和512x512数组产生的图片仍有明显差别。这是由于,生成一张给定大小的图片,如果输入的数据小于或大于该图片尺寸,plt.imshow()将执行插值操作。默认的插值是线性插值,可以看出效果并不总是理想的。可以通过imshow()函数的可选参数interpolation指定要使用的插值类型。

使用双三次插值算法(interpolation = ‘bicubic’)查看效果:

双三次插值算法

二维标量场的可视化

可以使用numpy.meshgrid() 函数从2D函数中生成样本。然后,使用plt.pcolormesh()显示此函数图形:

1
2
3
4
5
6
7
python复制代码n = 256
x = np.linspace(-3., 3., n)
y = np.linspace(-3., 3., n)
x_list, y_list = np.meshgrid(x, y)
z_list = x_list * np.cos(x_list ** 2 + y_list ** 2)
plt.pcolormesh(x_list, y_list, z_list, cmap = cm.Spectral)
cb = plt.colorbar(orientation='horizontal', shrink=.75)

二维标量场的可视化

Tips:使用颜色映射可以帮助我们快速判断相应点的符号和大小。

np.meshgrid()函数的作用是:获取两个坐标列表,并构建坐标网格。因为两个坐标列表都是numpy数组,所以我们可以以处理单个变量的方式处理它们,这使得计算标量场的过程简洁易读。最后,调用函数plt.pcolormesh()呈现图片。

等高线的可视化

等高线将具有相同值的所有点连接起来,可以更容易看到数据的分布特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码def iter_count(c, max_iter):
x = c
for n in range(max_iter):
if abs(x) > 2.:
return n
x = x ** 2 + 0.98 * c
return max_iter
n = 512
max_iter = 80
xmin, xmax, ymin, ymax = -0.32, -0.22, 0.8, 0.9
x = np.linspace(xmin, xmax, n)
y = np.linspace(ymin, ymax, n)
z = np.empty((n, n))
for i, y_i in enumerate(y):
for j, x_j in enumerate(x):
z[j, i] = iter_count(complex(x_j, y_i), max_iter)
plt.imshow(z, cmap = cm.Spectral,
interpolation = 'bicubic',
origin = 'lower',
extent=(xmin, xmax, ymin, ymax))
levels = [8, 12, 16, 20]
ct = plt.contour(x, y, z, levels, cmap = cm.binary)
plt.clabel(ct, fmt='%d')

等高线的可视化

Tips:pyplot.contour()函数获取样本网格的坐标列表x和y以及存储在矩阵z中的值。然后,该函数将渲染在"level"列表中指定的值相对应的轮廓,可以使用可选参数cmap运用色彩映射进行着色,也可以使用可选参数color为所有轮廓指定一种唯一的颜色。

每个轮廓可以用颜色条显示,也可以直接在图形上显示。plt.contour()函数返回一个Contour实例。pyplot.clabel()函数获取contour实例和一个可选的格式字符串来呈现每个等高线的标签。

Tips:默认情况下,填充轮廓不具抗锯齿性。可以使用了可选参数antialiased来获得更令人满意的结果。

1
python复制代码ct = plt.contour(x, y, z, levels, cmap = cm.binary, antialiased = True)

抗锯齿效果

二维向量场的可视化

向量场将二维向量与二维平面的每个点相关联,在物理学中很常见。
本例中,为了进行符号计算,我们借助SymPy包,这个软件包只用于保持代码的简短。如果未安装此包,可以使用pip install sympy命令进行安装。
我们不必关系向量场的计算方法,记住,本文的主要目的是可视化,因此我们只需要关心如何显示向量场——使用pyplot.quiver()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import sympy
from sympy.abc import x, y
def cylinder_stream_function(u = 1, r = 1):
radius = sympy.sqrt(x ** 2 + y ** 2)
theta = sympy.atan2(y, x)
return u * (radius - r ** 2 / r) * sympy.sin(theta)
def velocity_field(psi):
u = sympy.lambdify((x, y), psi.diff(y), 'numpy')
v = sympy.lambdify((x, y), -psi.diff(x), 'numpy')
return u, v
u_func, v_func = velocity_field(cylinder_stream_function() )
xmin, xmax, ymin, ymax = -2.5, 2.5, -2.5, 2.5
y, x = np.ogrid[ymin:ymax:16j, xmin:xmax:16j]
u, v = u_func(x, y), v_func(x, y)
m = (x ** 2 + y ** 2) < 1.
u = np.ma.masked_array(u, mask = m)
v = np.ma.masked_array(v, mask = m)
shape = patches.Circle((0, 0), radius = 1., lw = 2., fc = 'w', ec = 'c', zorder = 0)
plt.gca().add_patch(shape)
plt.quiver(x, y, u, v, color='c', zorder = 1)
plt.axes().set_aspect('equal')

二维向量场

Tips:向量场存储在矩阵u和v中,我们从向量场中采样的每个向量的坐标;矩阵x和y表示样本位置。矩阵x、y、u和v被传递给pyplot.quiver(),即可呈现向量场。

系列链接

Matplotlib常见统计图的绘制

Matplotlib使用自定义颜色绘制统计图

Matplotlib控制线条样式和线宽

Matplotlib自定义样式绘制精美统计图

Matplotlib在图形中添加文本说明

Matplotlib在图形中添加注释

Matplotlib在图形中添加辅助网格和辅助线

Matplotlib添加自定义形状

Matplotlib控制坐标轴刻度间距和标签

Matplotlib使用对数刻度和极坐标

Matplotlib绘制子图

Matplotlib自定义统计图比例

Matplotlib图形的输出与保存

本文转载自: 掘金

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

【Oracle小技巧】手把手教你玩转SQL Plus命令行,

发表于 2021-11-02

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

在这里插入图片描述

前言

  • 经常使用Oracle数据库的朋友,应该对SQL*Plus这个命令行工具不会陌生。每天工作都离不开它,但是这个工具有些缺点:
  • Linux系统下SQL*PLUS无法上下文查看历史命令,敲错命令需要按住Ctrl才能删除
  • SQL查询,输出结果格式错乱,每次都需要手动SET调整
  • 当前会话不显示实例名和登录用户,提示不人性化

注意:以上问题均为SQLPlus默认配置下。

那么问题来了,这些都可以解决吗?当然,我写这篇就是为了介绍如何优化SQL*Plus命令行嘛!

首先介绍下,主要分两个部分:

  • 上下文切换:rlwrap + readline
  • 优化输出格式:glogin.sql

SQL*Plus优化

1 上下文切换 rlwrap

  • 相信大家在Linux主机使用SQL*Plus命令行工具时,经常会遇到命令输错不好回退,或者刚输入的命令想再次执行,无法通过上下翻页切换的情况。
  • 上面的情况曾经也一直困惑着我,后来我发现了解决方案,这就来分享给大家,希望能帮助到你。通过 rlwrap + readline 一起使用,可以完美解决这个问题,接下来,我就来演示一下如何配置使用。

1、Linux主机配置yum源

1
2
3
4
bash复制代码##查看系统版本
cat /etc/system-release
##上传对应主机版本iso文件
scp rhel-server-7.9-x86_64-dvd.iso root@10.211.55.110:/soft

在这里插入图片描述

1
2
3
4
5
6
7
8
9
bash复制代码##挂载系统iso镜像源
mount -o loop /soft/rhel-server-7.9-x86_64-dvd.iso /mnt
##配置yum镜像源
mv /etc/yum.repos.d/* /tmp/
echo "[local]" >> /etc/yum.repos.d/local.repo
echo "name = local" >> /etc/yum.repos.d/local.repo
echo "baseurl = file:///mnt/" >> /etc/yum.repos.d/local.repo
echo "enabled = 1" >> /etc/yum.repos.d/local.repo
echo "gpgcheck = 0" >> /etc/yum.repos.d/local.repo

在这里插入图片描述
在这里插入图片描述
通过以上步骤,我们已经成功挂载系统镜像,可以开始安装redline。

2、安装readline依赖包

1
bash复制代码yum install -y readline*
  • 如果没有系统ISO镜像源,也可以直接在上直接下载readline安装包进行安装。

下载readline包:

1
bash复制代码wget -c ftp://ftp.gnu.org/gnu/readline/readline-6.2.tar.gz

在这里插入图片描述
上传安装包:

1
bash复制代码scp readline-6.2.tar.gz root@10.211.55.110:/soft

在这里插入图片描述
解压安装:

1
2
3
bash复制代码tar -zxvf readline-6.2.tar.gz
cd readline-6.2
./configure && make && make install

3、rlwrap安装

1
2
3
bash复制代码tar -xvf rlwrap-0.42.tar.gz
cd rlwrap-0.42
./configure && make && make install

下载地址:github.com/hanslub42/r…

注意:由于我macOS的终端连接可以切换回退,所以无法演示,以下使用XShell来进行演示。

  • 未使用rlwrap时,无法回退和切换上下文:
    在这里插入图片描述
  • 使用rlwrap时,可任意切换回退:
    在这里插入图片描述
    通过上述演示,已经可以轻松做到命令输错无需按住Ctrl键回退和上下文历史命令切换,可以大大提升工作效率。

4、配置环境变量

  • 为避免每次都需要输入rlwrap来调用命令,我们通过alias别名来配置环境变量实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码##配置oracle用户环境变量
cat <<EOF>>/home/oracle/.bash_profile
alias sqlplus='rlwrap sqlplus'
alias rman='rlwrap rman'
alias lsnrctl='rlwrap lsnrctl'
alias asmcmd='rlwrap asmcmd'
alias adrci='rlwrap adrci'
alias ggsci='rlwrap ggsci'
alias dgmgrl='rlwrap dgmgrl'
EOF

##环境变量生效
exit
su - oracle

在这里插入图片描述
至此,rlwrap工具就配置完成啦!

2 优化输出格式 glogin.sql

SQL*Plus 在启动时会自动运行脚本:glogin.sql 。

  • glogin.sql 存放在目录$ORACLE_HOME/sqlplus/admin/下。
  • 每当用户启动 SQLPlus 会话并成功建立 Oracle 数据库连接时,SQLPlus 就会执行此脚本。
  • 该脚本可以写入在 SQL*Plus 脚本中的任何内容,例如系统变量设置或 DBA 想要实现的其他全局设置。

1、未做配置时,默认如下:
在这里插入图片描述
此时,我登录SQL*PLUS并执行sql查询,看一下输出结果格式。

演示:未配置glogin.sql时,查询结果输出:
在这里插入图片描述
可以看到,查询结果格式很乱,而且连进去之后也看不到当前实例名和用户名。

2、配置glogin.sql

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
sql复制代码cat <<EOF>>$ORACLE_HOME/sqlplus/admin/glogin.sql
--设置编辑器用vi打开,windows客户端可以换成NotePad
define _editor=vi
--设置dbms_output输出缓冲区大小
set serveroutput on size 1000000
--设置输出格式
set long 200
set linesize 500
set pagesize 9999
--去除重定向输出每行拖尾空格
set trimspool on
--设置name列长
col Name format a80
--查询当前实例名
set termout off
col global_name new_value gname
define gname=idle
column global_name new_value gname
select lower(user) || '@' || substr( global_name, 1, decode( dot, 0,
length(global_name), dot-1) ) global_name
from (select global_name, instr(global_name,'.') dot from global_name );
set sqlprompt '&gname _DATE> '
--设置session时间格式
ALTER SESSION SET nls_date_format = 'HH24:MI:SS';
set termout on
EOF

在这里插入图片描述
演示:配置完glogin.sql时,查询结果输出:
在这里插入图片描述
通过以上配置,SQL*PLUS连接后,明显输出格式更加好看,显示更加人性化。具体配置可根据个人常用进行配置,比如可以将查询表空间使用率配置进去,每次打开都可以看到表空间使用率,防止数据文件撑爆。

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
sql复制代码--查询表空间使用率
col TABLESPACE_NAME for a20
select tbs_used_info.tablespace_name,
tbs_used_info.alloc_mb,
tbs_used_info.used_mb,
tbs_used_info.max_mb,
tbs_used_info.free_of_max_mb,
tbs_used_info.used_of_max || '%' used_of_max_pct
from (select a.tablespace_name,
round(a.bytes_alloc / 1024 / 1024) alloc_mb,
round((a.bytes_alloc - nvl(b.bytes_free,
0)) / 1024 / 1024) used_mb,
round((a.bytes_alloc - nvl(b.bytes_free,
0)) * 100 / a.maxbytes) used_of_max,
round((a.maxbytes - a.bytes_alloc + nvl(b.bytes_free,
0)) / 1048576) free_of_max_mb,
round(a.maxbytes / 1048576) max_mb
from (select f.tablespace_name,
sum(f.bytes) bytes_alloc,
sum(decode(f.autoextensible,
'YES',
f.maxbytes,
'NO',
f.bytes)) maxbytes
from dba_data_files f
group by tablespace_name) a,
(select f.tablespace_name,
sum(f.bytes) bytes_free
from dba_free_space f
group by tablespace_name) b
where a.tablespace_name = b.tablespace_name(+)) tbs_used_info
order by tbs_used_info.used_of_max desc;

--查询备份
col status for a10
col input_type for a20
col INPUT_BYTES_DISPLAY for a10
col OUTPUT_BYTES_DISPLAY for a10
col TIME_TAKEN_DISPLAY for a10

select input_type,
status,
to_char(start_time,
'yyyy-mm-dd hh24:mi:ss'),
to_char(end_time,
'yyyy-mm-dd hh24:mi:ss'),
input_bytes_display,
output_bytes_display,
time_taken_display,
COMPRESSION_RATIO
from v$rman_backup_job_details
where start_time > date '2021-07-01'
order by 3 desc;

在这里插入图片描述
至此,glogin.sql已经配置完成,欢迎食用👏🏻。

写在最后

glogin.sql 需要谨慎配置,没有理解的命令尽量不要写入。

大名鼎鼎的比特币勒索病毒,有一种方式就是通过glogin.sql来进行注入。

参考官方文档:

Configuring SQL*Plus:docs.oracle.com/cd/E11882_0…


本次分享到此结束啦~

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

本文转载自: 掘金

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

【爬虫系列】session和cookie的区别总结

发表于 2021-11-02

cookie:

特征:

  1. Cookie是存储在客户端的。
  2. Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息。
  3. Cookie具有不可跨域名性(浏览器访问百度不会带上谷歌的cookie)。
  4. 会话Cookie(一般存在内存)和持久Cookie(一般存在硬盘)。

创建:

用户A购买了一件商品放入购物车内,当用户B也购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。


客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,客户端会把Cookie保存起来。


当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

session:

特征:

  1. Session是存储在服务器端的,理论上是没有是没有限制,只要你的内存够大。
  2. Session是在无状态的HTTP协议下,服务端记录用户状态时用于标识具体用户的机制。
  3. Session 的运行依赖Session ID,而 Session ID 是存在 Cookie 中的,也就是说,如果浏览器禁用了 Cookie,Session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 Session ID)。
  4. 用户验证这种场合一般会用 Session。因此,维持一个会话的核心就是客户端的唯一标识,即Session ID。
  5. session是基于Cookie技术实现,重启浏览器后再次访问原有的连接依然会创建一个新的session,因为Cookie在关闭浏览器后就会消失,但是原来服务器的Session还在,只有等到了销毁的时间会自动销毁

创建:

当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了sessionId,如果已包含则说明以前已经为此客户端创建过session。


服务器就按照sessionId把这个session检索出来使用(检索不到,会新建一个),如果客户端请求不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关

联的sessionId。

sessionId的值是一个既不会重复,又不容易被找到规律以仿造的字符串,这个sessionId将被在本次响应中返回给客户端保存。

共同点:

  1. Cookie和Session都是会话跟踪技术

本文转载自: 掘金

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

聊了聊宏内核和微内核,并吹了一波 Linux

发表于 2021-11-02

👋👋点我!会使您的阅读体验更好😊

内核是操作系统非常重要的组成部分,同时也是操作系统的核心。内核管理着系统资源,内核向上连接着应用程序,向下连接着硬件,它是应用程序和硬件的桥梁。

内核可以进一步的划分,分为宏内核和微内核。

宏内核和微内核最大的区别就是,宏内核的用户服务和内核服务都保存在相同的地址空间中,它们都由内核进行统一管理,而微内核的用户服务和内核服务会保存在不同的地址空间中,下图可以很好的解释这一点。

image-20211027222024063

其实这里的宏内核翻译过来有点牵强,其实应该叫单内核或者单核。在这种单核的设计中,内核是一个大的整体,可以说是一个大进程,在这个大进程中,所有内核服务都运行在一个地址空间中,函数之间的调用链路少,直接通信简单高效。

而微内核的功能会划分为独立的进程,进程之间通过 IPC 进行通信,高度模块化,一个服务的故障不会影响另一个服务。不过由于模块化的影响,函数之间调用链路偏长,进程之间不会直接通信,而是通过内核服务相互通信。

从内核大小上面来讲,微内核的尺寸更小,只包含用户进程相关的服务,而单核的尺寸要比微内核大的多,这点比较好理解,因为宏内核融入了太多服务和驱动。

从执行效率上来说,微内核的执行效率相对较慢,因为涉及到跨模块调用,而宏内核执行效率高,因为函数之间会直接调用。

在微内核模块化之后,它很容易扩展,因为内核空间与用户空间相互隔离,在用户态下(运行在用户空间中的应用程序)应用程序崩溃后一般不会影响到内核中的数据。宏内核的可拓展性较差。

经过上面这些描述之后,我们很容易把宏内核和微内核的特征想象成软件开发中的单体架构和微服务架构。

单体架构最大的特点就是函数调用方便,几乎不存在调用链路,一个项目解决所有问题,项目中包含数据库驱动、各种拦截器、控制器、权限控制,可拓展性非常差。

而微服务的架构之间的调用链路会比较长,模块之间的职责分离并且相互依赖,比如权限控制模块、路由模块、总线通信模块。可拓展性比较强。

这两种不同的内核结构有不同的支持者,就和有些人认为单体架构好,有些人认为微服务架构模式好。

这就像对编程语言的争论一样,你说 Python 、Go、Java 以及其他语言哪个好?管他哪个好,最终都会戏谑的称 PHP 是这个世界上最好的语言。所以,这些争论本没有意义,但是很有趣的是,这种争论常常让人想起前几年在 CPU 领域中 RISC 和 CISC 之间的斗争。

现代成功的 CPU 设计包括这两种技术中的任何一种,就像 Linux 内核是微内核和宏内核的混合产品一样。可能有些人认为 Linux 它不就是个宏内核结构么,但实际上 Linux 不单单只是一个纯碎的集成内核。

为什么 Linux 会使用单内核(此处叫单内核有点应景)结构呢?我猜有下面几个因素。

从 Linus 的角度来看,单内核的开发和选型更容易,因为避免了与消息传递架构、计算模块加载方法等相关的工作。而且 Linux 的诞生原因在于 Linus 对 MINIX(一种类 UNIX 操作系统)只允许在教育上使用很不满,再加上 Linus 本来对操作系统很感兴趣,于是他开始编写 Linux 操作系统,所以我认为当时的 Linus 开发 Linux 起源于兴趣,并未经过详细周到的设计,也并未考虑它的可拓展性。当然这只是鄙人粗浅的猜测。

这就和我们上大学的毕业设计一样,你毕业设计做的系统,你会考虑可拓展性吗?除非你想当产品来做,但是何必呢?

另一个原因是充足的开发时间。Linux 没有研发时间限制,也没有发布时间表。任何限制都只能单独修改和扩展内核。核心的单一设计内部完全模块化,在这种情况下修改或添加不是很困难。问题是没有必要为了追求未经证实的可维护性的小幅增加而重写 Linux 内核。Linus 一再强调以下观点:为了这个好处而损失速度是不值得的。

Linux 是一个借鉴了微内核精髓的宏内核结构,Linux 支持模块化的设计、抢占式内核、对内核线程的支持以及动态加载内核模块的能力。不仅如此,Linux 还避免了其微内核设计的性能损失,允许一切运行在内核模式下,直接调用函数,无需消息传递。

所以综合一点来讲,Linux 是一个模块化、多线程和内核可调度的操作系统。

模块化的设计:Linux 支持内核模块的动态加载,尽管 Linux 内核也是单核,但它允许在需要时动态删除和加载一些内核代码。

可抢占性:Linux 内核支持可抢占,与传统的 UNIX 不同,Linux 内核具有允许内核中运行的任务优先执行的能力。在各种 UNIX 产品中,只有 Solaris 和 IRIX 支持抢占,但大多数传统 UNIX 内核不支持抢占。

在 Linux 身上,完美体现了务实性。如果一项功能没有价值或创意不佳,则不会开始实施。相反,在 Linux 的发展过程中,形成了一种值得称道的务实态度:任何改变都必须针对现实中实际存在的问题,需要经过完整的设计和正确简洁的实现。

如果 Linux 是纯微内核设计,那么移植到其他架构会更容易。实际情况是,Linux 内核移植虽然不是很简单,但也绝非不可能完成的事情。

最后给大家推荐一下我自己的Github,里面有非常多的硬核文章,绝对会对你有帮助。淦!!!

本文转载自: 掘金

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

用 Rust 实现 Lisp 解释器

发表于 2021-11-02
  • 文章标题:用 Rust 实现 Lisp 解释器
  • 深度参考:stopachka.essay.dev/post/5/risp…
  • 本文作者:suhanyujie
  • 文章来自:github.com/suhanyujie/…
  • ps:水平有限,翻译不当之处,还请指正,谢谢!

前言

一段时间没有写 Rust 了,感觉有些生疏了,打算找个 Rust 小项目复习一下。在芽之家博客看到了这个博文,讲的是用 Rust 实现 lisp。有感兴趣的同学,可以一起看看。

作者介绍到,这是他的第一个练手项目,有些地方可能会实现的不是很好,但我觉得也是很有参考价值的,尤其是对于我这样的 Rust 新手。此外,作者还提到了另一篇 python 实现 lisp,这应该也是参考资料之一。

Lisp

在开始前,我们需要了解一些关于 lisp 的背景知识。Lisp 是一种高阶编程语言,在其基础上演变出了很多种方言,如:Scheme、Common Lisp 等。查阅了下百度百科,其描述可读性不强,建议阅读维基百科的描述,或者这个 Lisp 教程。

在实现一个 Lisp(子集)的解析器之前,先要了解 Lisp 的语法规则。如果你想大概了解一下它的语法和简单使用,可以自己在本地安装一个环境,并尝试。这里以 Ubuntu 20.04 为例。可通过以下命令安装一个 common lisp 的实现 —— sbcl,用于熟悉 lisp:

1
arduino复制代码sudo apt-get install sbcl

然后,在命令行中输入 sbcl,即可进入它的交互式命令行:

1
2
3
4
5
6
7
8
csharp复制代码$ sbcl
This is SBCL 2.0.1.debian, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.

输入一个加法运算试一试:

1
2
ruby复制代码$ * (+ 1 2)
3

可以看到,能得到计算后地结果 —— 3。

关于更多关于 Lisp 的语法在这里就不详细说明了,可以参考这个教程进行进一步学习。

Lisp 的算术运算

为了能尽快地实现目标,我们只是简单地实现一个类似于计算器的运算功能,别看只是一个小小的计算器,但也包含了很多的基础知识。

在开始之前,我们先确定好最终的目标,我们最终实现的效果如下:

1
2
scss复制代码(+ 10 5) //=> 15
(- 10 5) //=> 5

输入简单的 lisp 程序,就能输出对应的计算结果。在开始之前,先介绍一下我们的程序执行,所经历的大体过程:

程序 -> parse(解析) -> 抽象语法树 -> eval(执行) -> 结果

这个过程中的 parse 和 eval 就是我们要实现的功能。比如下面这个程序示例:

1
2
3
4
ruby复制代码$ (+ 1 2)
3
$ (* 2 3)
6

换句话说,就是我们需要将我们输入的源代码解析转换成语法树,然后执行语法树就能得到我们想要的结果。而源码中,我们只需有三类输入:

  • 符号
  • 数值
  • 列表

将其用 Rust 枚举类型表示,如下:

1
2
3
4
5
6
rust复制代码#[derive(Clone)]
enum RispExp {
Symbol(String),
Number(f64),
List(Vec<RispExp>),
}

你可能有些疑惑,没关系,我们继续向后看。

在解析源码时,我们会遇到错误,因此需要定义错误类型:

1
2
3
Rust复制代码enum RispErr {
Reason(String),
}

如果你想定义更健壮、好用的错误类型,可以参考这个。但这里,为了简化实现,我们只是将错误类型定义成一个枚举变体 Reason(String),一旦遇到异常,我们将异常信息装入其中,返回给调用方即可。

我们还需要一个作用域类型,用它来存储定义的变量、内置函数等。

1
2
3
4
rust复制代码#[derive(Clone)]
struct RispEnv {
data: HashMap<String, RispExp>,
}

解析

根据前面的过程描述,我们要将源码解析成语法树,也就是 RispExp 的表示形式。这样做之前,我们需要将源码解析成一个一个 token。

比如我们的输入是 (+ 10 5),将其 token 化的结果是 ["(", "+", "10", "5", ")"]。使用 Rust 实现如下:

1
2
3
4
5
6
7
rust复制代码fn tokenize(expr: String) -> Vec<String> {
expr.replace("(", " ( ")
.replace(")", " ) ")
.split_whitespace()
.map(|x| x.to_string())
.collect()
}

根据 lisp 表达式的规则,表达式一般都是由小括号包裹起来的,为了更好的通过空格分割 token,我们将小括号替换为两边各带有一个空格的括号。然后通过 split_whitespace 函数将字符串进行分割,并把每段字符串转换成带所有权的字符串,最后通过 collect 收集,以字符串数组的形式存放到变量中。

然后通过 parse 函数将其转化成 RispExp 类型结构:

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
rust复制代码fn parse<'a>(tokens: &'a [String]) -> Result<(RispExp, &'a [String]), RispErr> {
let (token, rest) = tokens
.split_first()
.ok_or(RispErr::Reason("could not get token".to_string()))?;
match &token[..] {
"(" => read_seq(rest),
")" => Err(RispErr::Reason("unexpected `)`".to_string())),
_ => Ok((parse_atom(token), rest)),
}
}

fn read_seq<'a>(tokens: &'a [String]) -> Result<(RispExp, &'a [String]), RispErr> {
let mut res: Vec<RispExp> = vec![];
let mut xs = tokens;
loop {
let (next_token, rest) = xs
.split_first()
.ok_or(RispErr::Reason("could not find closing `)`".to_string()))?;
if next_token == ")" {
return Ok((RispExp::List(res), rest));
}
let (exp, new_xs) = parse(&xs)?;
res.push(exp);
xs = new_xs;
}
}

得到 token 列表后,我们对 token 逐个解析,通过 split_first 取出 token 列表中的第一个 token,以及第一个以外的其余元素。
对第一个 token 进行模式匹配:

  • 如果表达式以 ( 开头,则调用 read_seq 读取表达式剩余部分的 token
  • 如果表达式以 ) 开头,则意味着当前表达式是错误的表达式。
  • 以上之外,则是要按正常情况解析 lisp 表达式中的原子 —— atom。parse_atom 的实现如下:
1
2
3
4
5
6
7
rust复制代码fn parse_atom(token: &str) -> RispExp {
let potential_float: Result<f64, ParseFloatError> = token.parse();
match potential_float {
Ok(v) => RispExp::Number(v),
Err(_) => RispExp::Symbol(token.to_string().clone()),
}
}

根据语法规则,一个原子是一个数字连续字符或字符串,它包括数字和特殊字符。
我们先尝试将其解析为数值类型,如果解析失败,则意味着它是字符串 —— RispExp::Symbol(token.to_string().clone())。

我们会在全局符号表中存储变量的定义和函数定义,因此我们需要扩展一下 RispExp:

1
2
3
4
5
6
7
rust复制代码#[derive(Clone)]
enum RispExp {
Symbol(String),
Number(f64),
List(Vec<RispExp>),
Func(fn(&[RispExp]) -> Result<RispExp, RispErr>), // new
}

我们先创建一个存储特定符号的容器,每一个符号都有特殊的功能:

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
rust复制代码fn default_env() -> RispEnv {
let mut data: HashMap<String, RispExp> = HashMap::new();
data.insert(
"+".to_string(),
RispExp::Func(|args: &[RispExp]| -> Result<RispExp, RispErr> {
let sum = parse_list_of_floats(args)?
.iter()
.fold(0.0, |sum, a| sum + a);
Ok(RispExp::Number(sum))
}),
);
data.insert(
"-".to_string(),
RispExp::Func(|args: &[RispExp]| -> Result<RispExp, RispErr> {
let floats = parse_list_of_floats(args)?;
let first = *floats
.first()
.ok_or(RispErr::Reason("expected at least one number".to_string()))?;
let sum_of_rest = floats[1..].iter().fold(0.0, |sum, a| sum + a);

Ok(RispExp::Number(first - sum_of_rest))
}),
);

RispEnv { data }
}

这里我们先实现 +、- 运算符的功能。并且为了简化实现,我们先简单粗暴地认为参数都是合法的数值类型,可以通过 parse_list_of_floats 解析这些参数:

1
2
3
4
5
6
7
8
9
10
rust复制代码fn parse_list_of_floats(args: &[RispExp]) -> Result<Vec<f64>, RispErr> {
args.iter().map(|x| parse_single_float(x)).collect()
}

fn parse_single_float(exp: &RispExp) -> Result<f64, RispErr> {
match exp {
RispExp::Number(num) => Ok(*num),
_ => Err(RispErr::Reason("expect a number".to_string())),
}
}

执行

接下来是实现 eval(程序执行)部分了。

  • 1.程序体(表达式)的第一部分如果是标识符,则在全局环境中查询该标识符,如果存在,则返回(如果是 +、- 等操作符,则返回 RispExp::Func 类型的操作逻辑实现)。
  • 2.如果是数值,则返回该数值
  • 3.如果是列表,则尝试步骤一。即先返回 RispExp::Func(函数类型),然后列表中的其他原子作为参数执行该函数。
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
rust复制代码fn eval(exp: &RispExp, env: &mut RispEnv) -> Result<RispExp, RispErr> {
match exp {
RispExp::Symbol(k) => env
.data
.get(k)
.ok_or(RispErr::Reason(format!("unexpected symbol k={}", k)))
.map(|x| x.clone()),
RispExp::Number(_a) => Ok(exp.clone()),
RispExp::List(list) => {
let first_form = list
.first()
.ok_or(RispErr::Reason("expected a non-empty list".to_string()))?;
let arg_forms = &list[1..];
let first_eval = eval(first_form, env)?;
match first_eval {
RispExp::Func(f) => {
let args_eval = arg_forms
.iter()
.map(|x| eval(x, env))
.collect::<Result<Vec<RispExp>, RispErr>>();
f(&args_eval?)
}
_ => Err(RispErr::Reason("first form must be a function".to_string())),
}
}
RispExp::Func(_) => Err(RispErr::Reason("unexpected form".to_string())),
}
}

前面提到过,我们要实现一个简单的计算器,而 lisp 的计算表达式一般是以符号原子开始的,如:(+ 1 2)。
当把这个表达式转换为 RispExp 结构后的形式类似于:

1
2
3
4
5
6
scss复制代码// 伪代码
PlusFunc(
num1,
num2,
...
)

我们先通过 + 匹配到事先在 default_env 中注册好的函数 f,然后向该函数中传入第一个原子之后的所有参数:f(num1, num2),就能得到执行结果。

REPL

REPL 的全称是 Read Evel Print Loop,表示一种交互形式:读取 -> 执行 -> 打印结果 -> 循环。

针对前面实现的 lisp 子集,我们可以为其实现一个 repl,用于更好的使用该“lisp 解释器”。

我们要做的很简单,读取用户输入,然后解析执行,把执行结果打印出来,然后不断地循环整个过程。那接下来,把解释器的实现用循环包裹起来试试:

1
2
3
4
5
rust复制代码fn parse_eval(expr: String, env: &mut RispEnv) -> Result<RispExp, RispErr> {
let (parsed_exp, _) = parse(&tokenize(expr))?;
let evaled_exp = eval(&parsed_exp, env)?;
Ok(evaled_exp)
}

获取用户输入的表达式,再调用 parse_eval:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rust复制代码fn slurp_expr() -> String {
let mut expr = String::new();
io::stdin()
.read_line(&mut expr)
.expect("Failed to read line");
expr
}

pub fn run_repl() {
let env = &mut default_env();
loop {
println!("risp >");
let expr = slurp_expr();
match parse_eval(expr, env) {
Ok(res) => println!("// 🔥 => {}", res),
Err(e) => match e {
RispErr::Reason(msg) => println!("// 🙀 => {}", msg),
},
}
}
}

好了,接下来我们把 run_repl 放入 main 函数中:

1
2
3
rust复制代码fn main() {
run_repl();
}

大功告成!我们只需在命令行中输入 cargo run 即可启动你的 repl 程序。完整的代码可以点此查看。

启动后,输入简单的 lisp 表达式,看看效果:

1
2
3
4
5
6
scss复制代码risp >
(+ 1 2 )
// 🔥 => 3
risp >
(+ 1 10 (+ 20 1))
// 🔥 => 32

可以看出,单一的表达式和嵌套的表达式的加、减法都可以正确地计算出结果。这样,我们算是实现了这个简单的加减法计算。

版本 0.1.1

目前,我们的“lisp”仅支持简单的加、减等算数运算,我们需要扩展它。先给它增加 bool 类型的支持。

1
2
3
4
5
6
7
8
rust复制代码#[derive(Clone)]
enum RispExp {
Symbol(String),
Number(f64),
List(Vec<RispExp>),
Func(fn(&[RispExp]) -> Result<RispExp, RispErr>),
Bool(bool), // ->new
}

对应的我们需要调整 parse_atom 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rust复制代码fn parse_atom(token: &str) -> RispExp {
match token {
"true" => {
RispExp::Bool(true)
},
"false" => {
RispExp::Bool(false)
},
_ => {
let potential_float: Result<f64, ParseFloatError> = token.parse();
match potential_float {
Ok(v) => RispExp::Number(v),
Err(_) => RispExp::Symbol(token.to_string().clone()),
}
}
}
}

有了布尔类型之后,我们可以实现 >,<,= 等比较运算符,因为通过这些运算符计算后的结果值是布尔值。

要能支持这些比较运算符,我们需要将 = 对应的处理逻辑加到 default_env 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rust复制代码// = 逻辑实现
data.insert(
"=".to_string(),
RispExp::Func(|args: &[RispExp]| -> Result<RispExp, RispErr> {
let floats = parse_list_of_floats(args)?;
// 要想比较,需要有两个值
if floats.len() != 2 {
return Err(RispErr::Reason("expected two number".to_string()));
}
// 将第 0 个元素和第 1 个元素进行比较
if floats.get(0).is_none() || floats.get(1).is_none() {
return Err(RispErr::Reason("expected number".to_string()));
}
let is_ok = floats.get(0).unwrap().eq(floats.get(1).unwrap());
Ok(RispExp::Bool(is_ok))
}),
);

此时,我们的 lisp 解释器已经支持了 = 的操作,使用 cargo run 运行 repl:

1
2
3
4
5
6
7
scss复制代码risp >
(= 12 12)
// 🔥 => true
risp >
(= 1 2 3)
// 🙀 => expected two number
risp >

真不错,我们实现了 = 操作的扩展支持。我们还需要继续支持 >、>=、<、<=。以 >= 为例,将其实现加入到 default_env 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rust复制代码data.insert(
">=".to_string(),
RispExp::Func(|args: &[RispExp]| -> Result<RispExp, RispErr> {
let floats = parse_list_of_floats(args)?;
// 要想比较,需要有两个值
if floats.len() != 2 {
return Err(RispErr::Reason("expected two number".to_string()));
}
// 校验这两个值必须存在
if floats.get(0).is_none() || floats.get(1).is_none() {
return Err(RispErr::Reason("expected number".to_string()));
}
Ok(RispExp::Bool(
floats.get(0).unwrap().gt(floats.get(1).unwrap()),
))
}),
);

根据原博客,为了简化代码,这部分的实现可以用宏实现:

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
rust复制代码macro_rules! ensure_tonicity {
($check_fn:expr) => {{
|args: &[RispExp]| -> Result<RispExp, RispErr> {
let floats = parse_list_of_floats(args)?;
let first = floats
.first()
.ok_or(RispErr::Reason("expected at least one number".to_string()))?;
let rest = &floats[1..];
fn f(prev: &f64, xs: &[f64]) -> bool {
match xs.first() {
Some(x) => $check_fn(prev, x) && f(x, &xs[1..]),
None => true,
}
};
Ok(RispExp::Bool(f(first, rest)))
}
}};
}

data.insert(
">".to_string(),
RispExp::Func(ensure_tonicity!(|a, b| a > b)),
);

data.insert(
"<".to_string(),
RispExp::Func(ensure_tonicity!(|a, b| a < b)),
);

data.insert(
"<=".to_string(),
RispExp::Func(ensure_tonicity!(|a, b| a <= b)),
);

这样就实现了所有比较运算符的处理逻辑了。

要实现一个更接近 lisp 的语言,我们还需要引入 def 和 if 这两关键字了。这两关键字的作用见下表:

图片来自知乎专栏

因此,我们先更新 eval 函数,使其优先匹配内置标识符(关键字),如果不是关键字,则直接按照原先逻辑执行:

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
rust复制代码fn eval(exp: &RispExp, env: &mut RispEnv) -> Result<RispExp, RispErr> {
match exp {
...
...

RispExp::List(list) => {
let first_form = list
.first()
.ok_or(RispErr::Reason("expected a non-empty list".to_string()))?;
let arg_forms = &list[1..];
// 优先匹配并处理“关键字”
match eval_built_in_form(first_form, arg_forms, env) {
Some(built_in_res) => built_in_res,
None => {
let first_eval = eval(first_form, env)?;
match first_eval {
RispExp::Func(f) => {
let args_eval = arg_forms
.iter()
.map(|x| eval(x, env))
.collect::<Result<Vec<RispExp>, RispErr>>();
f(&args_eval?)
}
_ => Err(RispErr::Reason("first form must be a function".to_string())),
}
}
}
}
RispExp::Func(_) => Err(RispErr::Reason("unexpected form".to_string())),
}
}

// 处理内置标识符
fn eval_built_in_form(
exp: &RispExp,
other_args: &[RispExp],
env: &mut RispEnv,
) -> Option<Result<RispExp, RispErr>> {
match exp {
RispExp::Symbol(symbol) => match symbol.as_ref() {
"if" => Some(eval_if_args(other_args, env)),
"def" => Some(eval_def_args(other_args, env)),
_ => None,
},
_ => None,
}
}

fn eval_if_args(args: &[RispExp], env: &mut RispEnv) -> Result<RispExp, RispErr> {
let test_form = args
.first()
.ok_or(RispErr::Reason("expected test form".to_string()))?;
let test_eval = eval(test_form, env)?;
match test_eval {
RispExp::Bool(b) => {
let form_idx = if b { 1 } else { 2 };
let res_form = args
.get(form_idx)
.ok_or(RispErr::Reason(format!("expected form idx={}", form_idx)))?;
let res_eval = eval(res_form, env);
res_eval
}
_ => Err(RispErr::Reason(format!(
"unexpected test form='{}'",
test_form.to_string()
))),
}
}

根据上图表格中的描述,if 语法如下:(if test conseq alt),对 test 表达式求值,如果为真,则对 conseq 表达式求值并返回;否则,对 alt 表达式求值并返回。例如:(if (> 10 20) (+ 2 3) (- 1 2))。

同理,def 语法:(def var exp)。用于定义一个新的变量 var,它的值是 exp 表达式的值,例如:(def k1 10)。逻辑实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rust复制代码fn eval_def_args(args: &[RispExp], env: &mut RispEnv) -> Result<RispExp, RispErr> {
let var_exp = args.first().ok_or(RispErr::Reason(format!("unexepceted string for var")))?;

let val_res = args.get(1).ok_or(RispErr::Reason(format!("expected second param.")))?;
let evaled_val = eval(val_res, env)?;

match var_exp {
RispExp::Symbol(ref var_name) => {
env.data.insert(var_name.clone(), evaled_val);
Ok(var_exp.clone())
},
_ => Err(RispErr::Reason(format!("unexpected var name")))
}
}

我们运行 repl(cargo run),通过一些输入,看看实现的效果:

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码risp >
(def a 1)
// 🔥 => a
risp >
(+ 1 a)
// 🔥 => 2
risp >
(if (> 2 1) true false)
// 🔥 => true
risp >
(if (< 2 1) true false)
// 🔥 => false

太棒了,一切都运行的很完美!

接下来,我们尝试支持另一种语法 —— lambda。下面是一个翻译文章对 lambda 的描述:

lambda 特殊形式会创建一个过程(procedure)。(lambda这个名字来源于Alonzo Church的lambda calculus) —— 来自译文

lambda 其实就是一种匿名函数,既然是函数也就意味着有参数列表和函数体,所以,lambda 的语法形式如下:(lambda (var...) exp),其中的 (var...) 是参数列表,exp 是函数体,因此我们定义 lambda 结构体:

1
2
3
4
5
rust复制代码#[derive(Clone)]
struct RispLambda {
params: Rc<RispExp>,
body: Rc<RispExp>,
}

解析 lambda 表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码fn eval_lambda_args(args: &[RispExp]) -> Result<RispExp, RispErr> {
let params = args
.first()
.ok_or(RispErr::Reason(format!("unexpected args form")))?;
let body = args
.get(1)
.ok_or(RispErr::Reason(format!("unexpected second form")))?;
if args.len() != 2 {
return Err(RispErr::Reason(format!("lambda can only have two forms")));
}
Ok(RispExp::Lambda(RispLambda {
params: Rc::new(params.clone()),
body: Rc::new(body.clone()),
}))
}

对用户的输入进行解析,基于已经解析了的 RispExp 结构,当遇到的 List 是 lambda 类型时,将跟随在 lambda 后的第一个表达式视为“参数列表”,第二个表达式视为“lambda 函数体”。然后返回一个 RispExp::Lambda 实例。

当 lambda 被调用时,会生成一个不同于 default_env 的新 env,可将其视为当前函数的作用域,当执行函数体的时候,会使用新的 env 中的符号、参数等信息,如果查找不到,则在全局环境(default_env)中查找,所以需要调整一下 RispEnv:

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
rust复制代码struct RispEnv<'a> {
data: HashMap<String, RispExp>,
outer: Option<&'a RispEnv<'a>>,
}

/// 构建 lambda 执行环境
fn env_for_lambda<'a>(
params: Rc<RispExp>,
args: &[RispExp],
outer_env: &'a mut RispEnv,
) -> Result<RispEnv<'a>, RispErr> {
let ks = parse_list_of_symbol_strings(params)?;
if ks.len() != args.len() {
return Err(RispErr::Reason(format!(
"expected {} params, got {}",
ks.len(),
args.len()
)));
}
let vs = eval_forms(args, outer_env)?;
let mut data: HashMap<String, RispExp> = HashMap::new();
for (k, v) in ks.iter().zip(vs.iter()) {
data.insert(k.clone(), v.clone());
}

Ok(RispEnv {
data,
outer: Some(outer_env),
})
}

/// 执行一组表达式,将结果放入数组中
fn eval_forms(args: &[RispExp], env: &mut RispEnv) -> Result<Vec<RispExp>, RispErr> {
args.iter().map(|x| eval(x, env)).collect()
}

/// 解析参数列表
fn parse_list_of_symbol_strings(params: Rc<RispExp>) -> Result<Vec<String>, RispErr> {
let list = match params.as_ref() {
RispExp::List(s) => Ok(s.clone()),
_ => Err(RispErr::Reason(format!("expected params to be a list"))),
}?;
list.iter()
.map(|x| match x {
RispExp::Symbol(s) => Ok(s.clone()),
_ => Err(RispErr::Reason(format!(
"expected symbol in the argument list"
))),
})
.collect()
}

env_for_lambda 函数中的 data 是 lambda 内部环境,outer 则是外层(全局环境)env。
通过构建好的 lambda body,将其基于新构建的 lambda 环境执行,得到的结果即 lambda 调用结果。

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
scss复制代码fn eval(exp: &RispExp, env: &mut RispEnv) -> Result<RispExp, RispErr> {
...

RispExp::List(list) => {
...

match eval_built_in_form(first_form, arg_forms, env) {
Some(built_in_res) => built_in_res,
None => {
let first_eval = eval(first_form, env)?;
match first_eval {
RispExp::Func(f) => {
let args_eval = arg_forms
.iter()
.map(|x| eval(x, env))
.collect::<Result<Vec<RispExp>, RispErr>>();
f(&args_eval?)
}
RispExp::Lambda(lambda) => { // -> New
let new_env = &mut env_for_lambda(lambda.params, arg_forms, env)?;
eval(&lambda.body, new_env)
},
_ => Err(RispErr::Reason("first form must be a function".to_string())),
}
}
}
}
}

基本完成了 lambda 的支持,我们编译代码试试吧!(cargo run)

1
2
3
4
5
6
7
8
9
sql复制代码risp >
(def add-one (lambda (a) (+ a 1)))
// 🔥 => add-one
risp >
(add-one 1)
// 🔥 => 2
risp >
(add-one 5)
// 🔥 => 6

REPL 中,我们通过 def 定义了一个名为 add-one 的 lambda 表达式。
然后调用 add-one,传入的参数为 1,结果为 2,入参为 5 时,计算结果为 6。符合预期!

至此,lambda 表达式支持完成!完整的代码可以点此查看。

Lisp 是非常早期的高阶编程语言之一,它的出现开创了很多先驱概念,如:树、动态类型、高阶函数等。它结构简单,却是计算机语言发展中非常重要的基础。本文通过使用 Rust 实现 Lisp 子集,即是学习 Lisp 本身,也是学习 Rust 的语法和使用。基于此,你可以探索更加完整的 Lisp 实现。希望对读者你有帮助,感谢阅读!

参考资料

  • stopachka.essay.dev/post/5/risp…
  • 如何(用Python)写一个(Lisp)解释器(译文) zhuanlan.zhihu.com/p/28989326
  • lisp-lang.org/learn/getti…
  • mod 作用域,关于 Rust 模块详解,可以查看这篇文章
  • Lisp 教程 www.yiibai.com/lisp/lisp_o…
  • Lisp 维基百科 zh.wikipedia.org/wiki/LISP

本文转载自: 掘金

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

MySQL备份系列-- mysqldump备份(全量+增量)

发表于 2021-11-02

在日常运维工作中,对mysql数据库的备份是万分重要的,以防在数据库表丢失或损坏情况出现,可以及时恢复数据。

线上数据库备份场景:

每周日执行一次全量备份,然后每天下午1点执行MySQLdump增量备份.

下面对这种备份方案详细说明下:

1.MySQLdump增量备份配置

执行增量备份的前提条件是MySQL打开binlog日志功能,在my.cnf中加入

log-bin=/opt/Data/MySQL-bin

“log-bin=”后的字符串为日志记载目录,一般建议放在不同于MySQL数据目录的磁盘上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码`-----------------------------------------------------------------------------------`

`mysqldump >       导出数据`

`mysql <           导入数据  (或者使用``source``命令导入数据,导入前要先切换到对应库下)`

 

`注意一个细节:`

`若是mysqldump导出一个库的数据,导出文件为a.sql,然后mysql导入这个数据到新的空库下。`

`如果新库名和老库名不一致,那么需要将a.sql文件里的老库名改为新库名,`

`这样才能顺利使用mysql命令导入数据(如果使用``source``命令导入就不需要修改a.sql文件了)。`

`-----------------------------------------------------------------------------------`

2.MySQLdump增量备份

假定星期日下午1点执行全量备份,适用于MyISAM存储引擎。

[root@test-huanqiu ~]# MySQLdump –lock-all-tables –flush-logs –master-data=2 -u root -p test > backup_sunday_1_PM.sql

对于InnoDB将–lock-all-tables替换为–single-transaction

–flush-logs为结束当前日志,生成新日志文件;

–master-data=2 选项将会在输出SQL中记录下完全备份后新日志文件的名称,

用于日后恢复时参考,例如输出的备份SQL文件中含有:

CHANGE MASTER TO MASTER_LOG_FILE=’MySQL-bin.000002′, MASTER_LOG_POS=106;

3.MySQLdump增量备份其他说明:

如果MySQLdump加上–delete-master-logs 则清除以前的日志,以释放空间。但是如果服务器配置为镜像的复制主服务器,用MySQLdump –delete-master-logs删掉MySQL二进制日志很危险,因为从服务器可能还没有完全处理该二进制日志的内容。在这种情况下,使用 PURGE MASTER LOGS更为安全。

每日定时使用 MySQLadmin flush-logs来创建新日志,并结束前一日志写入过程。并把前一日志备份,例如上例中开始保存数据目录下的日志文件 MySQL-bin.000002 , …

1.恢复完全备份

mysql -u root -p < backup_sunday_1_PM.sql

2.恢复增量备份

mysqlbinlog MySQL-bin.000002 … | MySQL -u root -p注意此次恢复过程亦会写入日志文件,如果数据量很大,建议先关闭日志功能

–compatible=name

它告诉 MySQLdump,导出的数据将和哪种数据库或哪个旧版本的 MySQL 服务器相兼容。值可以为 ansi、MySQL323、MySQL40、postgresql、oracle、mssql、db2、maxdb、no_key_options、no_tables_options、no_field_options 等,要使用几个值,用逗号将它们隔开。当然了,它并不保证能完全兼容,而是尽量兼容。

–complete-insert,-c

导出的数据采用包含字段名的完整 INSERT 方式,也就是把所有的值都写在一行。这么做能提高插入效率,但是可能会受到 max_allowed_packet 参数的影响而导致插入失败。因此,需要谨慎使用该参数,至少我不推荐。

–default-character-set=charset

指定导出数据时采用何种字符集,如果数据表不是采用默认的 latin1 字符集的话,那么导出时必须指定该选项,否则再次导入数据后将产生乱码问题。

–disable-keys

告诉 MySQLdump 在 INSERT 语句的开头和结尾增加 /*!40000 ALTER TABLE table DISABLE KEYS /; 和 /!40000 ALTER TABLE table ENABLE KEYS */; 语句,这能大大提高插入语句的速度,因为它是在插入完所有数据后才重建索引的。该选项只适合 MyISAM 表。

–extended-insert = true|false

默认情况下,MySQLdump 开启 –complete-insert 模式,因此不想用它的的话,就使用本选项,设定它的值为 false 即可。

–hex-blob

使用十六进制格式导出二进制字符串字段。如果有二进制数据就必须使用本选项。影响到的字段类型有 BINARY、VARBINARY、BLOB。

–lock-all-tables,-x

在开始导出之前,提交请求锁定所有数据库中的所有表,以保证数据的一致性。这是一个全局读锁,并且自动关闭 –single-transaction 和 –lock-tables 选项。

–lock-tables

它和 –lock-all-tables 类似,不过是锁定当前导出的数据表,而不是一下子锁定全部库下的表。本选项只适用于 MyISAM 表,如果是 Innodb 表可以用 –single-transaction 选项。

–no-create-info,-t

只导出数据,而不添加 CREATE TABLE 语句。

–no-data,-d

不导出任何数据,只导出数据库表结构。

mysqldump –no-data –databases mydatabase1 mydatabase2 mydatabase3 > test.dump

将只备份表结构。–databases指示主机上要备份的数据库。

–opt

这只是一个快捷选项,等同于同时添加 –add-drop-tables –add-locking –create-option –disable-keys –extended-insert –lock-tables –quick –set-charset 选项。本选项能让 MySQLdump 很快的导出数据,并且导出的数据能很快导回。该选项默认开启,但可以用 –skip-opt 禁用。注意,如果运行 MySQLdump 没有指定 –quick 或 –opt 选项,则会将整个结果集放在内存中。如果导出大数据库的话可能会出现问题。

–quick,-q

该选项在导出大表时很有用,它强制 MySQLdump 从服务器查询取得记录直接输出而不是取得所有记录后将它们缓存到内存中。

–routines,-R

导出存储过程以及自定义函数。

–single-transaction

该选项在导出数据之前提交一个 BEGIN SQL语句,BEGIN 不会阻塞任何应用程序且能保证导出时数据库的一致性状态。它只适用于事务表,例如 InnoDB 和 BDB。本选项和 –lock-tables 选项是互斥的,因为 LOCK TABLES 会使任何挂起的事务隐含提交。要想导出大表的话,应结合使用 –quick 选项。

–triggers

同时导出触发器。该选项默认启用,用 –skip-triggers 禁用它。

跨主机备份

使用下面的命令可以将host1上的sourceDb复制到host2的targetDb,前提是host2主机上已经创建targetDb数据库:

-C 指示主机间的数据传输使用数据压缩

mysqldump –host=host1 –opt sourceDb| mysql –host=host2 -C targetDb

结合Linux的cron命令实现定时备份

比如需要在每天凌晨1:30备份某个主机上的所有数据库并压缩dump文件为gz格式

30 1 * * * mysqldump -u root -pPASSWORD –all-databases | gzip > /mnt/disk2/database_date '+%m-%d-%Y'.sql.gz

一个完整的Shell脚本备份MySQL数据库示例。比如备份数据库opspc

[root@test-huanqiu ~]# vim /root/backup.sh

#!bin/bash

echo “Begin backup mysql database”

mysqldump -u root -ppassword opspc > /home/backup/mysqlbackup-date +%Y-%m-%d.sql

echo “Your database backup successfully completed”

[root@test-huanqiu ~]# crontab -e

30 1 * * * /bin/bash -x /root/backup.sh > /dev/null 2>&1

mysqldump全量备份+mysqlbinlog二进制日志增量备份

1)从mysqldump备份文件恢复数据会丢失掉从备份点开始的更新数据,所以还需要结合mysqlbinlog二进制日志增量备份。

首先确保已开启binlog日志功能。在my.cnf中包含下面的配置以启用二进制日志:

[mysqld]

log-bin=mysql-bin

2)mysqldump命令必须带上–flush-logs选项以生成新的二进制日志文件:

mysqldump –single-transaction –flush-logs –master-data=2 > backup.sql

其中参数–master-data=[0|1|2]

0: 不记录

1:记录为CHANGE MASTER语句

2:记录为注释的CHANGE MASTER语句

mysqldump全量+增量备份方案的具体操作可参考下面两篇文档:

数据库误删除后的数据恢复操作说明

解说mysql之binlog日志以及利用binlog日志恢复数据


下面分享一下自己用过的mysqldump全量和增量备份脚本

应用场景:

1)增量备份在周一到周六凌晨3点,会复制mysql-bin.00000*到指定目录;

2)全量备份则使用mysqldump将所有的数据库导出,每周日凌晨3点执行,并会删除上周留下的mysq-bin.00000*,然后对mysql的备份操作会保留在bak.log文件中。

脚本实现:

1)全量备份脚本(假设mysql登录密码为123456;注意脚本中的命令路径):

[root@test-huanqiu ~]# vim /root/Mysql-FullyBak.sh\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码#!/bin/bash\
# Program\
# use mysqldump to Fully backup mysql data per week!\
# History\
# Path\
BakDir=/home/mysql/backup\
LogFile=/home/mysql/backup/bak.log\
Date=`date +%Y%m%d`\
Begin=`date +"%Y年%m月%d日 %H:%M:%S"`\
cd $BakDir\
DumpFile=$Date.sql\
GZDumpFile=$Date.sql.tgz\
/usr/local/mysql/bin/mysqldump -uroot -p123456 --quick --events --all-databases --flush-logs --delete-master-logs --single-transaction > $DumpFile\
/bin/tar -zvcf $GZDumpFile $DumpFile\
/bin/rm $DumpFile\
Last=`date +"%Y年%m月%d日 %H:%M:%S"`\
echo 开始:$Begin 结束:$Last $GZDumpFile succ >> $LogFile\
cd $BakDir/daily\
/bin/rm -f *

2)增量备份脚本(脚本中mysql的数据存放路径是/home/mysql/data,具体根据自己的实际情况进行调整)

[root@test-huanqiu ~]# vim /root/Mysql-DailyBak.sh\

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
bash复制代码#!/bin/bash\
# Program\
# use cp to backup mysql data everyday!\
# History\
# Path\
BakDir=/home/mysql/backup/daily                     //增量备份时复制mysql-bin.00000*的目标目录,提前手动创建这个目录\
BinDir=/home/mysql/data                                   //mysql的数据目录\
LogFile=/home/mysql/backup/bak.log\
BinFile=/home/mysql/data/mysql-bin.index           //mysql的index文件路径,放在数据目录下的\
/usr/local/mysql/bin/mysqladmin -uroot -p123456 flush-logs\
#这个是用于产生新的mysql-bin.00000*文件\
Counter=`wc -l $BinFile |awk '{print $1}'`\
NextNum=0\
#这个for循环用于比对$Counter,$NextNum这两个值来确定文件是不是存在或最新的\
for file in `cat $BinFile`\
do\
    base=`basename $file`\
    #basename用于截取mysql-bin.00000*文件名,去掉./mysql-bin.000005前面的./\
    NextNum=`expr $NextNum + 1`\
    if [ $NextNum -eq $Counter ]\
    then\
        echo $base skip! >> $LogFile\
    else\
        dest=$BakDir/$base\
        if(test -e $dest)\
        #test -e用于检测目标文件是否存在,存在就写exist!到$LogFile去\
        then\
            echo $base exist! >> $LogFile\
        else\
            cp $BinDir/$base $BakDir\
            echo $base copying >> $LogFile\
         fi\
     fi\
done\
echo `date +"%Y年%m月%d日 %H:%M:%S"` $Next Bakup succ! >> $LogFile

3)设置crontab任务,执行备份脚本。先执行的是增量备份脚本,然后执行的是全量备份脚本:

[root@test-huanqiu ~]# crontab -e

#每个星期日凌晨3:00执行完全备份脚本

0 3 * * 0 /bin/bash -x /root/Mysql-FullyBak.sh >/dev/null 2>&1

#周一到周六凌晨3:00做增量备份

0 3 * * 1-6 /bin/bash -x /root/Mysql-DailyBak.sh >/dev/null 2>&1

4)手动执行上面两个脚本,测试下备份效果

[root@test-huanqiu backup]# pwd

/home/mysql/backup

[root@test-huanqiu backup]# mkdir daily

[root@test-huanqiu backup]# ll

total 4

drwxr-xr-x. 2 root root 4096 Nov 29 11:29 daily

[root@test-huanqiu backup]# ll daily/

total 0

先执行增量备份脚本

[root@test-huanqiu backup]# sh /root/Mysql-DailyBak.sh

[root@test-huanqiu backup]# ll

total 8

-rw-r–r–. 1 root root 121 Nov 29 11:29 bak.log

drwxr-xr-x. 2 root root 4096 Nov 29 11:29 daily

[root@test-huanqiu backup]# ll daily/

total 8

-rw-r—–. 1 root root 152 Nov 29 11:29 mysql-binlog.000030

-rw-r—–. 1 root root 152 Nov 29 11:29 mysql-binlog.000031

[root@test-huanqiu backup]# cat bak.log

mysql-binlog.000030 copying

mysql-binlog.000031 copying

mysql-binlog.000032 skip!

2016年11月29日 11:29:32 Bakup succ!

然后执行全量备份脚本

[root@test-huanqiu backup]# sh /root/Mysql-FullyBak.sh

20161129.sql

[root@test-huanqiu backup]# ll

total 152

-rw-r–r–. 1 root root 145742 Nov 29 11:30 20161129.sql.tgz

-rw-r–r–. 1 root root 211 Nov 29 11:30 bak.log

drwxr-xr-x. 2 root root 4096 Nov 29 11:30 daily

[root@test-huanqiu backup]# ll daily/

total 0

[root@test-huanqiu backup]# cat bak.log

mysql-binlog.000030 copying

mysql-binlog.000031 copying

mysql-binlog.000032 skip!

2016年11月29日 11:29:32 Bakup succ!

开始:2021年11月01日 11:30:38 结束:2021年11月01日 11:30:38 20211101.sql.tgz succ

本文转载自: 掘金

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

1…437438439…956

开发者博客

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