学习 准备 尝试 谨慎小心

0%

日期时间对象

关于日期时间的操作可以分为两种:

  • 转换:与字符串的互相转换,与时间戳的互相转换
  • 计算:计算两个时间点之间的间隔、时间点与时间段的计算(计算下周N、下个月D日、去年M月D日等等)

Java8 提供了三个类:LocalDateLocalTimeLocalDateTime,它们的形式如 2020-01-0112:30:002020-01-01 12:30:00

创建对象

获取类对象的方法非常非常简单

1
LocalDate now = LocalDate.now();
2
LocalDate ld = LocalDate.of(2019, 1, 1);
3
// 获取年月日
4
now.getYear();
5
now.getMonthValue();	// 如果你调用了 now.getMonth() ,那么它将返回给你一个大写的英文月份单词
6
now.getDayOfMonth();
7
// 顾名应该思义
8
getDayOfWeek();
9
getDayOfYear();	
10
11
// 设置年月日
12
LocalDate ld1 = ld.withYear(2021);		// 2021-01-01
13
LocalDate ld2 = ld.withMonth(12);		// 2019-12-01
14
LocalDate ld3 = ld.withDayOfMonth(12);	// 2019-12-12
15
// 你可能会纳闷,既然是设置,为什么不用单词 set 呢,而用 with
16
// 因为,set 操作一般是改变调用对象本身,没有返回值;
17
// 而 with 是在调用对象基础上另外创建一个新对象,设置好值后返回,没有改变调用对象
18
19
// 如果你是那个打破砂锅的孩子,你可能会问:为什么不能改变调用对象?
20
// 因为 LocalDate 是 final 修饰的(final 人称 Java 界的自宫之刀)
21
// 从物理的角度来讲,目前人类无法改变时间(穿越)
22
23
// 如果你有 ld.withMonth(13) 这种反人类历法的操作,当然是会抛出异常的

LocalTimeLocalDateTime 都有类似于 LocalDate 的方法,这里就不一一列举了(因为我感觉自己越来越像 api 文档了)

Java8 API 官方文档直通车

转换

日期时间对象 和 字符串 之间的互相转换:

1
// LocalDateTime 对象 -> 字符串
2
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
3
LocalDateTime now = LocalDateTime.now();
4
String dateTimeStr = now.format(dtf);
5
System.out.println(dateTimeStr);
6
7
// 字符串 -> LocalDateTime 对象
8
String str = "2022-01-30 12:15:20";
9
LocalDateTime dateTime = LocalDateTime.parse(str, dtf);
10
System.out.println(dateTime);

DateTimeFormatter 类还提供一些现成的 formatter ,比如

1
DateTimeFormatter.BASIC_ISO_DATE ==> DateTimeFormatter.ofPattern("yyyyMMdd")
2
DateTimeFormatter.ISO_LOCAL_DATE ==> DateTimeFormatter.ofPattern("yyyy-MM-dd")
3
// 更多 formatter 可以 api 文档中查询

学习的本质,不在于记住哪些知识,而在于它触发了你的思考。—— 迈克尔·桑德尔

日期时间 和 时间戳 之间的互相转换:

1
// LocalDateTime 对象 -> 时间戳
2
LocalDateTime now = LocalDateTime.now();
3
// 获取系统默认时区
4
ZoneId systemDefaultZoneId = ZoneId.systemDefault();
5
Instant instant = now.atZone(systemDefaultZoneId).toInstant();
6
long timestamp = instant.toEpochMilli();
7
System.out.println(timestamp);
8
9
10
// 时间戳 -> LocalDateTime 对象
11
long timestamp2 = 1578919583784L;
12
Instant instant2 = Instant.ofEpochMilli(timestamp2);
13
LocalDateTime dateTime2 = LocalDateTime.ofInstant(instant2, systemDefaultZoneId);
14
System.out.println(dateTime2);

“我不明白为什么要把时间戳搞得这么麻烦!”

另外:java.util.Datejava.time.LocalDateTime 之间的转换需要通过 Instant 实现,它俩都没有提供直接的转换方法

1
// 获取系统默认时区
2
ZoneId systemDefaultZoneId = ZoneId.systemDefault();
3
// Date 转为 LocalDateTime
4
Date date3 = new Date();
5
Instant instant3 = date3.toInstant();
6
LocalDateTime localDateTime3 = LocalDateTime.ofInstant(instant3, systemDefaultZoneId);
7
8
// LocalDateTime 转为 Date
9
Instant instant4 = now.atZone(systemDefaultZoneId).toInstant();
10
Date date4 = Date.from(instant4);

还有:LocalDateTime 可以由 LocalDateLocalTime 组成,也可以拆分成它俩

1
LocalDate nowLocalDate = LocalDate.now();
2
LocalTime nowLocalTime = LocalTime.now();
3
LocalDateTime nowLocalDateTime = LocalDateTime.of(nowLocalDate, nowLocalTime);
4
5
nowLocalDateTime.toLocalDate();
6
nowLocalDateTime.toLocalTime();

计算

计算时间点与时间点之间的间隔:

1
// 计算日期时间之间的间隔
2
LocalTime startTime = LocalTime.now();
3
LocalTime endTime = startTime.plusHours(1).plusMinutes(50);
4
Duration duration = Duration.between(startTime, endTime);
5
6
// 间隔秒数
7
duration.getSeconds();
8
// 间隔天数
9
duration.toDays();
10
// 间隔小时数
11
duration.toHours();
12
// 间隔分钟数
13
duration.toMinutes();

Duration.between(start, end) 的参数可以是 LocalDateTimeLocalTime

它只会返回一个整数(舍掉小数后的整数,等同于 floor()),不会返回 1小时50分钟 这样的形式
如果你想要 y年M个月d天 H小时m分钟s秒 这种形式,或许你自己动手组装一下了

1
// 计算日期之间的间隔
2
LocalDate startDate = LocalDate.now();
3
LocalDate endDate = LocalDate.of(2031, 1, 1);
4
Period pe = Period.between(startDate, endDate);
5
pe.getYears();
6
pe.getMonths();
7
pe.getDays();

时间点与时间段的计算:

1
public LocalDateTime plusYears(long years) {
2
       LocalDate newDate = date.plusYears(years);
3
       return with(newDate, time);
4
   }
5
6
   public LocalDateTime plusMonths(long months) {
7
       LocalDate newDate = date.plusMonths(months);
8
       return with(newDate, time);
9
   }
10
11
   public LocalDateTime plusWeeks(long weeks) {
12
       LocalDate newDate = date.plusWeeks(weeks);
13
       return with(newDate, time);
14
   }
15
16
   public LocalDateTime plusDays(long days) {
17
       LocalDate newDate = date.plusDays(days);
18
       return with(newDate, time);
19
   }
20
21
   public LocalDateTime plusHours(long hours) {
22
       return plusWithOverflow(date, hours, 0, 0, 0, 1);
23
   }
24
25
   public LocalDateTime plusMinutes(long minutes) {
26
       return plusWithOverflow(date, 0, minutes, 0, 0, 1);
27
   }
28
29
   public LocalDateTime plusSeconds(long seconds) {
30
       return plusWithOverflow(date, 0, 0, seconds, 0, 1);
31
   }
32
33
   public LocalDateTime plusNanos(long nanos) {
34
       return plusWithOverflow(date, 0, 0, 0, nanos, 1);
35
   }
36
37
   public LocalDateTime minusYears(long years) {
38
       return (years == Long.MIN_VALUE ? plusYears(Long.MAX_VALUE).plusYears(1) : plusYears(-years));
39
   }
40
41
   public LocalDateTime minusMonths(long months) {
42
       return (months == Long.MIN_VALUE ? plusMonths(Long.MAX_VALUE).plusMonths(1) : plusMonths(-months));
43
   }
44
45
   public LocalDateTime minusWeeks(long weeks) {
46
       return (weeks == Long.MIN_VALUE ? plusWeeks(Long.MAX_VALUE).plusWeeks(1) : plusWeeks(-weeks));
47
   }
48
49
   public LocalDateTime minusDays(long days) {
50
       return (days == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-days));
51
   }
52
53
   public LocalDateTime minusHours(long hours) {
54
       return plusWithOverflow(date, hours, 0, 0, 0, -1);
55
  }
56
57
   public LocalDateTime minusMinutes(long minutes) {
58
       return plusWithOverflow(date, 0, minutes, 0, 0, -1);
59
   }
60
61
   public LocalDateTime minusSeconds(long seconds) {
62
       return plusWithOverflow(date, 0, 0, seconds, 0, -1);
63
   }
64
65
   public LocalDateTime minusNanos(long nanos) {
66
       return plusWithOverflow(date, 0, 0, 0, nanos, -1);
67
   }

看吧,加减年数、月数、天数、小时数、分钟数、秒数、毫秒数都有

想怎么用就怎么用,举个小例子:

1
LocalDateTime now = LocalDateTime.now();
2
3
// 30年后的今天(我还要上班,还没退休)
4
LocalDateTime after30Years = now.plusYears(30L);
5
System.out.println(after30Years);
6
7
// 347个月后(我就能还清贷款了 ╥﹏╥)
8
LocalDateTime after348Months = now.plusMonths(347L);
9
System.out.println(after348Months);
10
11
// 11天后(就是除夕了)
12
LocalDateTime after10Days = now.plusDays(10L);
13
System.out.println(after10Days);
14
15
// 8小时前(我在上班)
16
LocalDateTime before8Hours = now.minusHours(8L);
17
System.out.println(before8Hours);
18
19
// 3分钟前(我开始听 Let it go 这首歌)
20
LocalDateTime before3Before = now.minusMinutes(3L);
21
System.out.println(before3Before);
22
23
// 10秒前(写下下面这条代码)
24
LocalDateTime before10Second = now.minusSeconds(10L);
25
System.out.println(before10Second);

其实不用区分什么加减的,也可以用 plusXxx 做减法,只要传入负数参数就行了

另外这里还有一类需求,比如:

明年的感恩节是哪天?(每年11月的第四个星期四为感恩节)
下周五是哪天?

这就要用到时间校正器 TemporalAdjuster
这里容我先介绍一下 TemporalAdjuster,它是一个函数式接口,只有一个方法 adjustInto

1
@FunctionalInterface
2
public interface TemporalAdjuster {
3
    Temporal adjustInto(Temporal temporal);
4
}

Temporal 是一个接口,LocalDateTimeLocalDateLocalTime 都是它的实现类

LocalDateTimeLocalDateLocalTime 中都有 with(TemporalAdjuster adjuster) 这个方法用来实现上面提到的另类需求。

1
// 下周五
2
LocalDateTime now = LocalDateTime.now();
3
LocalDateTime nextFriday = now.with(dt -> {
4
    // dt 是 `Temporal` 对象,但实质上是调用对象的类型
5
    LocalDateTime dateTime = (LocalDateTime) dt;
6
    // 非常可惜,没有 withDayOfWeek() 这个方法,要不然就会非常方便了
7
    int dayOfWeekValue = dateTime.getDayOfWeek().getValue();
8
    int fridayValue = DayOfWeek.FRIDAY.getValue();
9
    return dateTime.plusWeeks(1L)
10
        		.plusDays(fridayValue - dayOfWeekValue);
11
});
12
System.out.println(nextFriday);
13
14
// 明年的感恩节(明年11月第四个星期四)
15
LocalDate thanksGivingDay = LocalDate.now().with( t -> {
16
    LocalDate d = (LocalDate) t;
17
    // 明年11月1日
18
    LocalDate newDate = d.plusYears(1L).withMonth(11).withDayOfMonth(1);
19
20
    int dayOfWeekValue = newDate.getDayOfWeek().getValue();
21
    int thursdayValue = DayOfWeek.THURSDAY.getValue();
22
    long plusWeeks = dayOfWeekValue > thursdayValue ? 4L : 3L;
23
    return newDate.plusWeeks(plusWeeks)
24
        			.plusDays(thursdayValue - dayOfWeekValue);
25
});
26
System.out.println(thanksGivingDay);

其实 TemporalAdjusters 提供了许多 TemporalAdjuster 对象,就像上一节 Stream 中 Collectors 之于 Collector 一样 。

使用 TemporalAdjusters 能够十分方便的实现上面的需求

1
// 下个周五
2
LocalDateTime nextFriday = LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
3
System.out.println(nextFriday);
4
5
// 明年的感恩节(明年11月第四个星期四)
6
LocalDate date = LocalDate.now().plusYears(1L).withMonth(11);
7
LocalDate thanksGivingDay = date.with(TemporalAdjusters.dayOfWeekInMonth(4, DayOfWeek.THURSDAY));
8
System.out.println(thanksGivingDay);

注意: TemporalAdjusters.next(DayOfWeek day) 方法返回的是 接下来第一个周五,并不是我们一般理解的 下周五,比如说:今天 2020-01-13(周一),那么返回的就是 2020-01-17 四天后的周五。

另外 TemporalAdjusters 并不止提供了上面这2个方法,还有很多其他方法, API 文档 中给出了足够多的例子,一看就明白了。

建议有兴趣的同学,去阅读一些源码,参考 Java8 代码逻辑,然后用其他编程语言实现相同的日期时间操作,因为在其他编程中(比如 javaScript)也会经常用到日期时间的操作

其他特性

罗列出来表示我知道他们,但不表示我理解他们,所以 Let It Go!

接口中的默认方法

接口中的静态方法

Optional 类

1
Optional<String> op = Optional.of(str);

它是用来标识这个变量有可能为空。

如果一个变量有可能为空,Java8 之前我们每次使用这个变量时,都必须判断它是否为空。现在也是!!

Optional 并不能避免空指针异常,仅仅是表示标识变量可能为空。打个比方:

前面一条路上埋了地雷,但从表面上完全看不出来,除非我们走一步都扔石头试一下,否则说不准哪一步就炸了。而 Optional就是用来标识地雷位置的,我们知道了哪个位置有雷,就会绕着走,从而能够安全通过

另外 Optional 还提供了一个设置默认值的功能,挺好玩的。

1
Integer pageSize = null;
2
// 以前我们设置默认值
3
//pageSize = pageSize == null ? 10 : pageSize;
4
//System.out.println(pageSize);
5
6
// 使用 Optional 设置默认值
7
pageSize = Optional.ofNullable(pageSize)
8
    .orElse(20);
9
System.out.println(pageSize);
10
11
// 自定义默认值
12
Integer defaultPageSize = Optional.ofNullable(pageSize)
13
        .orElseGet(() -> {
14
            return new Integer(50);
15
        });

结语

Java8 新特性系列随便到此就结束了。

最最关键的,还是多看 Java 官方 API 文档!!

我在想可能我们被矫枉过正了。各种各样技术群最多的回答都是:去问百度!! 百度全知道吗?!

说实在的,在今天这个时代,是个人都能在网络上发表文章言论,然后大家再互相转载,假的都能成为真的!
没有经过验证就转载的;在当时有效,现在过时了的;随便在文章中一个转载链接的 … 比比皆是

这里有一个开眼的短视频,介绍 虚假新闻是如何在传播

最恼人的是,百度搜索一个关键词 XXX,点进去一篇博客文章,结果文章内容根本就没有这个关键词,那么关键词到底在哪呢?
关键词在网站的推荐文章列表标题里,所以百度爬取到了,显示在搜索结果里了。真是日了狗了!!

百度搜到的博客能别看就别看,看也要看那些经常更新,比较靠谱的。

靠谱的学习途径:

  1. 官方最新 API 文档
  2. 书籍/视频(要注意权威性和时效性)
  3. 经常更新靠谱的博客

轻易不要百度!!

Stream 用来处理集合数据的,通过 stream 操作可以实现 SQL 的拥有的大部分查询功能

Java8 API 官方文档

下面借助例子,演示 stream 操作

Java userList 列表

1
private List<User> userList = Arrays.asList(
2
    new User(101, "小明", 10, "男", "青海省", "西宁市"),
3
    new User(102, "小青", 12, "女", "宁夏回族自治区", "银川市"),
4
    new User(103, "小海", 8, "男", "西藏自治区", "拉萨市"),
5
    new User(108, "阿刁", 18, "女", "西藏自治区", "拉萨市"),
6
    new User(104, "小阳", 9, "女", "新疆维吾尔自治区", "乌鲁木齐市"),
7
    new User(105, "小强", 14, "男", "陕西省", "西安市"),
8
    new User(106, "小帅", 15, "男", "河北省", "石家庄市"),
9
    new User(107, "小云", 15, "女", "河北省", "石家庄市")
10
);

MySQL user 表数据

1
DROP TABLE IF EXISTS `user`;
2
CREATE TABLE `user`  (
3
  `id` int(11) PRIMARY KEY,
4
  `name` varchar(20),
5
  `age` int(2),
6
  `gender` varchar(10),
7
  `province` varchar(100),
8
  `city` varchar(100)
9
) ;
10
11
INSERT INTO `user` VALUES (101, '小明', 10, '男', '青海省', '西宁市');
12
INSERT INTO `user` VALUES (102, '小青', 12, '女', '宁夏回族自治区', '银川市');
13
INSERT INTO `user` VALUES (103, '小海', 8, '男', '西藏自治区', '拉萨市');
14
INSERT INTO `user` VALUES (104, '小阳', 9, '女', '新疆维吾尔自治区', '乌鲁木齐市');
15
INSERT INTO `user` VALUES (105, '小强', 14, '男', '陕西省', '西安市');
16
INSERT INTO `user` VALUES (106, '小帅', 15, '男', '河北省', '石家庄市');
17
INSERT INTO `user` VALUES (107, '小云', 15, '女', '河北省', '石家庄市');

查询字段 select - map

1
// select id from user
2
userList.stream()
3
	.map(e -> e.getId())
4
	.forEach(System.out::println);

至于如何实现 select id, name from user 查询多字段在下面 collector 收集器会详细讲解

条件 where - filter

1
// select * from user where age<10
2
userList.stream()
3
        .filter(e-> e.getAge() < 10)
4
        .forEach(System.out::println);
5
6
// select * from user where age<10 and gender='男'
7
userList.stream()
8
        .filter(e->e.getAge() < 10)
9
        .filter(e->e.getGender()=="男")
10
        .forEach(System.out::println);

最值、总和、数量、均值(max, min, sum, count, average)

1
// select max(age), min(age), sum(age), count(age), avg(age) from user
2
// max
3
Optional<Integer> maxAge = userList.stream()
4
                                .map(e -> e.getAge())
5
                                .max(Comparator.comparingInt(x -> x));
6
// 等同于
7
// Optional<Integer> maxAge =  userList.stream()
8
//	.map(e -> e.getAge())
9
//	.max((x, y) -> x-y);
10
11
// min
12
Optional<Integer> minAge = userList.stream()
13
                                .map(e -> e.getAge())
14
                                .min(Comparator.comparingInt(x -> x));
15
// sum
16
Optional<Integer> sumAge = userList.stream()
17
                                .map(e -> e.getAge())
18
                                .reduce((e, u) -> e + u);
19
// count
20
long count = userList.stream()
21
                .map(e -> e.getAge())
22
                .count();
23
// 平均值=总和/数量

排序 order by - sorted

1
// select * from user order by age
2
userList.stream()
3
        .sorted(Comparator.comparingInt(User::getAge))
4
        .forEach(System.out::println);

分页 limit - skip、limit

1
// select * from user limit 5
2
userList.stream()
3
        .limit(5)
4
        .forEach(System.out::println);
5
6
// select * from user limit 5, 5
7
userList.stream()
8
        .skip(5)
9
        .limit(5)
10
        .forEach(System.out::println);
11
12
// select * from user order by age limit 1
13
userList.stream()
14
        .sorted(Comparator.comparingInt(User::getAge))
15
        .limit(1)
16
        .forEach(System.out::println);
17
// 或者
18
Optional<User> minAgeUser = userList.stream()
19
                                .sorted(Comparator.comparingInt(User::getAge))
20
                                .findFirst();

是否存在 exists - anymatch

1
// select exists(select * from user where name='小海')
2
// 有没有名字叫“小海”的用户
3
boolean exists0 = userList.stream()
4
						.anyMatch(e -> e.getName().equals("小海"));
5
6
// select not exists(select * from user where name='小海')
7
// 是不是没有名字叫“小海”的用户
8
boolean exists1 = userList.stream()
9
						.noneMatch(e -> e.getName().equals("小海"));
10
11
// 是不是所有用户年龄都小于10岁
12
boolean exists2 = userList.stream()
13
						.allMatch(e -> e.getAge() < 10);

收集操作 collect

收集操作就是遍历 stream 中的元素,并进行累加处理,即归约 reduction

归约的定义

A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list.

前面提到的 max() min() count() reduce() 都属于 reduction operation

collect() 又和前面这几种归约操作有所区别,它是 Mutable reduction 动态归约

动态归约的定义

A mutable reduction operation accumulates input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream

区别:动态归约将结果放进 Collection StringBuilder 这样的动态容器中,所以称为动态归约。

Stream 接口提供了两个 collect() 方法

1
<R> R collect(Supplier<R> supplier,
2
				BiConsumer<R, ? super T> accumulator,
3
				BiConsumer<R, R> combiner);
4
5
<R, A> R collect(Collector<? super T, A, R> collector);

我们只需理解了第一个方法,第二个方法就手到擒来了

理解第一个 collect 方法,强烈建议阅读文档 动态归约的定义,下面只简单的介绍一下它

三个参数:

  • 供给者 supplier:负责提供动态容器,例如 Collectors、StringBuilder
  • 累加器 accumulator:负责将流中的元素做累加处理
  • 合并者 combiner :负责将两个容器的元素合并在一起

在串行流中,combiner 根本没有执行,所以随便写点啥满足参数对象就行。
如果说串行流是单线程,那么并行流就是多线程了

举个例子:

1
2
    ArrayList<String> strings = new ArrayList<>();
3
    for (T element : stream) {
4
        strings.add(element.toString());
5
    }
6
// 等同于
7
ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
8
                                               (c, e) -> c.add(e.toString()),
9
                                               (c1, c2) -> c1.addAll(c2));

与其传递三个参数这么麻烦,还不如直接传递一个对象呢!
这就是第二个 collect() 方法的由来,使用收集器 Collector 来替代三个参数

实际上,我们一般不需要自己创建 Collector 对象,Java8 提供了一个 Collectors 类,专门提供收集器 Collector 对象。毕竟我们平时能够使用到的收集操作也就那几种:转为集合对象、分组、统计。

下面以例子演示


在初看 stream 操作的时候,我被什么创建、中间操作、终止操作、不会改变原对象给弄晕了,我根本不关心这些,我的第一想法是怎么将操作后的数据导出来,重新变成集合对象。

toCollection

不使用收集器的情况下:

1
List<User> subUserList1 = userList.stream()
2
				.filter(e -> e.getAge() < 10)
3
				.filter(e -> e.getGender() == "男")
4
				.collect(() -> new ArrayList<>(),
5
						(c, e) -> c.add(e),
6
						(c1, c2) -> c1.addAll(c2));

在 collect() 方法第二个参数累加器 accumulator (c, e) -> c.add(e) 这里,对流中元素进行了遍历,所以可以把流中元素添加到任意的集合容器中,List、Set、Map 等等

使用 Collectors 工具类提供的收集器:

1
// toList()
2
List<User> list = userList.stream()
3
				.filter(e -> e.getAge() < 10)
4
				.filter(e -> e.getGender() == "男")
5
				.collect(Collectors.toList());
6
7
// toSet()
8
Set<User> set = userList.stream()
9
				.filter(e -> e.getAge() < 10)
10
				.filter(e -> e.getGender() == "男")
11
				.collect(Collectors.toSet());
12
13
// toCollection(),想要返回什么容器,就 new 一个
14
ArrayList<User> collection = userList.stream()
15
				.filter(e -> e.getAge() < 10)
16
				.filter(e -> e.getGender() == "男")
17
				.collect(Collectors.toCollection(
18
                    () -> new ArrayList<>()
19
                ));

这里插播一条新闻:如何将流转为数组?

Stream 提供了方法 toArray()

1
Object[] toArray();
2
<A> A[] toArray(IntFunction<A[]> generator);

小试牛刀:

1
Object[] nameArray = userList.stream()
2
        .map(e -> e.getName())
3
        .toArray();
4
Arrays.stream(nameArray)
5
    	.forEach(System.out::println);
6
// 转为 User 对象数组
7
User[] users = userList.stream()
8
        .filter(e -> e.getGender() == "女")
9
        .toArray(User[]::new);
10
Arrays.stream(users)
11
    	.forEach(System.out::println);

toStringBuilder

不使用收集器的情况下:

1
StringBuilder joinName = userList.stream()
2
				.map(e -> e.getName())
3
				.collect(StringBuilder::new,
4
						(s, e) ->  s = s.length() > 0 ? s.append("-" + e) : s.append(e),
5
						(s1, s2) -> s1.append(s2)
6
				);

谁能告诉我在Java中怎么单独使用三元运算符?s = s.length() > 0 ? s.append("-" + e) : s.append(e) 我想把 s = 省略掉,但 Java 中不行

使用 Collectors 类提供的收集器:

1
String joinName1 = userList.stream()
2
				.map(e -> e.getName())
3
				.collect(Collectors.joining());
4
5
String joinName2 = userList.stream()
6
                .map(e -> e.getName())
7
                .collect(Collectors.joining("-"));
8
9
String joinName3 = userList.stream()
10
                .map(e -> e.getName())
11
                .collect(Collectors.joining("-", "[", "]"));

至于 Collectors.joining() 参数分别代表什么含义,看一下它们的参数名称,就明白了

1
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,	// 分隔符
2
                                                             CharSequence prefix,	// 前缀
3
                                                             CharSequence suffix) 	// 后缀

toMap

在 Collectors 中一共有3个 toMap(),它们用来处理不同的问题

两个参数的 toMap

1
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
2
                                   Function<? super T, ? extends U> valueMapper) {
3
       return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
4
   }

参数 keyMapper 用来获取key;valueMapper 用来获取 value

它的内部调用了四个参数的 toMap() 方法

例子

1
Map<Integer, User> map1 = userList.stream()
2
    .collect(Collectors.toMap(e -> e.getId(), Function.identity()));
3
System.out.println(map1);
4
// Function.identity() 等价于 e -> e
5
6
// select id, name, gender from user
7
Map<Integer, Map<String, Object>> map2 = userList.stream()
8
    .collect(Collectors.toMap(e -> e.getId(), e -> {
9
        Map<String, Object> map = new HashMap<>();
10
        map.put("gender", e.getGender());
11
        map.put("name", e.getName());
12
        map.put("id", e.getId());
13
        return map;
14
    }));
15
System.out.println(map2);

你:如果 key 冲突了咋办?
Java8:你想咋办就咋办

三个参数的 toMap

1
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
2
                                    Function<? super T, ? extends U> valueMapper,
3
                                    BinaryOperator<U> mergeFunction) {
4
        return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
5
    }

第三个参数 mergeFunction 就是用来处理 key 键冲突的

内部也是调用了四个参数的 toMap() 方法

例子

1
// 如果 key 冲突,那么将冲突的 value 值拼接在一起
2
Map<String, String> map3 = userList.parallelStream()
3
                                    .collect(Collectors.toMap(
4
                                        e -> e.getGender(), 
5
                                        e -> e.getName(), 
6
                                        (o1, o2) -> o1 + ", " + o2
7
                                        )
8
                                    );
9
System.out.println(map3);

你:我想自己 new 一个 Map 对象

四个参数的 toMap

1
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
2
                            Function<? super T, ? extends U> valueMapper,
3
                            BinaryOperator<U> mergeFunction,
4
                            Supplier<M> mapSupplier)

参数 mapSupplier 用来提供返回容器

例子

1
LinkedHashMap<String, String> map4 = userList.parallelStream()
2
				.collect(Collectors.toMap(
3
                    				e -> e.getGender(), 
4
                    				e -> e.getName(), 
5
                    				(o1, o2) -> o1 + ", " + o2, 
6
                    				LinkedHashMap::new
7
                				)
8
                		);
9
System.out.println(map4);

reducing

单参数和两参数的 reducing()

1
Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
2
Collector<T, ?, U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)

以例子具体解释这两个方法

1
Optional<String> names1 = userList.stream()
2
				.map(User::getName)
3
				.collect(Collectors.reducing((e1, e2) -> e1 + "," + e2));
4
System.out.println(names1.get());
5
6
// 等同于
7
String names2 = userList.stream()
8
    					.collect(Collectors.reducing(
9
                                "", 
10
                                (e) -> e.getName(), 
11
                                (e1, e2) -> e1 + "," + e2)
12
                   	 	);
13
System.out.println(names2);

输出结果:

1
小明,小青,小海,阿刁,小阳,小强,小帅,小云
2
,小明,小青,小海,阿刁,小阳,小强,小帅,小云

参数 identity 表示返回结果的初始值

三参数的 reducing()

1
reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)

identity 是初始值,mapper 会对元素先进行一次处理,然后 op 对元素进行归约操作

注意: 返回类型要和参数 identity 的一致。
你也许会纳闷,为什么有的返回一个 Optional<String> 类型数据,而有的就返回了 String
因为含有参数 identity 的 reduing 方法中返回值有初始值,也就是 identity,所以不会出现空的情况

下面Collectors 提供的一些常用归约收集器

1
// minBy、maxBy
2
Optional<User> minAgeUser = userList.stream()
3
				.collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));
4
5
// counting
6
Long count = userList.stream()
7
    .collect(Collectors.counting());
8
9
// summingInt、summingLong、summingDouble、
10
Integer sumAge = userList.stream()
11
    .collect(Collectors.summingInt(User::getAge));
12
13
// averagingInt、averagingLong、averagingDouble
14
// 平均值内部是总值/数量,所以返回值是浮点数 dobule
15
Double avgAge = userList.stream()
16
    .collect(Collectors.averagingInt(User::getAge));

你也许觉得每次都要执行一遍 minBy、maxBy、counting、summingXxx、averagingXxx 这些太麻烦了,有没有一次执行就获取所有这些方法结果?
有的。这就是 summarizingXxx

1
Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
2
Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
3
Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)

这里不演示了,实际上你看一下 XxxSummaryStatistics 这些类就明白了,比如

1
public class IntSummaryStatistics implements IntConsumer {
2
    private long count;
3
    private long sum;
4
    private int min = Integer.MAX_VALUE;
5
    private int max = Integer.MIN_VALUE;
6
  	...
7
}

group by

最最激动人心的时候到了,我们要使用分组了!!!

1
Map<String, List<User>> map = userList.stream()
2
				.collect(Collectors.groupingBy(User::getGender));

SQL 中的 group by 结果集中只能包含分组字段和聚合函数计算结果,这段代码比它更加全面

我们使用如下语句输出结果

1
map.keySet().stream()
2
        .forEach((e) -> {
3
        	System.out.println(e + "=" + map.get(e));
4
        });

显示结果:

1
女=[User{id=102, name='小青', age=12, gender='女', province='宁夏回族自治区', city='银川市'}, User{id=108, name='阿刁', age=18, gender='女', province='西藏自治区', city='拉萨市'}, User{id=104, name='小阳', age=9, gender='女', province='新疆维吾尔自治区', city='乌鲁木齐市'}, User{id=107, name='小云', age=15, gender='女', province='河北省', city='石家庄市'}]
2
男=[User{id=101, name='小明', age=10, gender='男', province='青海省', city='西宁市'}, User{id=103, name='小海', age=8, gender='男', province='西藏自治区', city='拉萨市'}, User{id=105, name='小强', age=14, gender='男', province='陕西省', city='西安市'}, User{id=106, name='小帅', age=15, gender='男', province='河北省', city='石家庄市'}]

它真的分组了!!这是真正的分组

那怎么对分组中的元素进行操作呢,像 SQL 那样??

完全不用担心,Collectors 提供了三个 groupBy 方法返回分组收集器

1
Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T,? extends K> classifier)
2
    
3
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T,? extends K> classifier, 
4
                                      Collector<? super T,A,D> downstream)
5
    
6
Collector<T, ?, M> groupingBy(Function<? super T,? extends K> classifier, 
7
                              Supplier<M> mapFactory, 
8
                              Collector<? super T,A,D> downstream)

让我们放飞想象的翅膀,思考一下这几个参数分别有什么用。

downstream ?有 down 就表示有 up。那么谁是 upstream,很明显是 userList.stream,那么 downstream 就是分组集合的流喽。猜测 downstream 收集器是对分组中的元素进行归约操作的,就像是分组 SQL 语句字段中的聚合操作一样。

1
// select gender, count(*) from user group by gender
2
Map<String, Long> map2 = userList.stream()
3
				.collect(Collectors.groupingBy(User::getGender, Collectors.counting()));
4
System.out.println(map2);

输出结果确实不出所料!这就是证明参数 downstream 确实是分组集合元素的收集器。

Supplier<M> mapFactory 这函数式接口方法不会有参数传入,所以不会操作集合元素;它只是返回一个变量。同志们,注意观察三个方法返回值,前二者都指定了 Map 作为归约操作的返回类型,而第三个要我们自己定义,使用 mapFactory 提供返回的数据容器

两参数的 groupingBy 方法其实是调用了三参数的 groupingBy 方法(而单参数 groupingBy 调用了两参数的 groupingBy)

1
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
2
                                      Collector<? super T, A, D> downstream) {
3
    return groupingBy(classifier, HashMap::new, downstream);
4
}

groupingBy 经常使用 Collectors.mapping() 处理分组集合

1
Map<String, List<Map<String, Object>>> map4 = userList.stream()
2
    .collect(Collectors.groupingBy(
3
        User::getGender,
4
        Collectors.mapping(e -> {
5
            Map<String, Object> m = new HashMap<>();
6
            m.put("name", e.getName());
7
            m.put("id", e.getId());
8
            return m;
9
        }, Collectors.toList())
10
    ));
11
System.out.println(map4);

输出结果:

1
{女=[{name=小青, id=102}, {name=阿刁, id=108}, {name=小阳, id=104}, {name=小云, id=107}], 男=[{name=小明, id=101}, {name=小海, id=103}, {name=小强, id=105}, {name=小帅, id=106}]}

partitionBy

实际上也是分组,只不过 partitionBy 是按照布尔值(真假)来分组

1
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
2
    return partitioningBy(predicate, toList());
3
}
4
5
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
6
                                                    Collector<? super T, A, D> downstream)

例子:大于10岁为一组,小于等于10的为一组

1
Map<Boolean, List<User>> map1 = userList.stream()
2
				.collect(Collectors.partitioningBy(e -> e.getAge() > 10));

例子:统计大于10岁的有多少人,小于等于10岁的有多少人

1
Map<Boolean, Long> map2 = userList.stream()
2
    		.collect(Collectors.partitioningBy(e -> e.getAge() > 10, Collectors.counting()));

第二个参数 downstream 用来处理分组集合

结语

Java8 提供的 stream 几乎是穷尽了所有集合元素能有的操作,起码是穷尽了我脑海里对集合元素操作的所有想象

这篇文章也列举了 stream 绝大部分的功能,尽量写得通俗易懂,但读者理解起来可能还是有模糊的地方,这时建议大家参考 Java8 API 官方文档 ,多做几个 Demo 加深理解

不要过度使用

stream 是为了方便集合操作,简化代码而推出的,提升代码执行效率并不是它的目的。

虽然,并行流会对代码的执行效率有较大的提升(尤其是数据量非常大的时候),但也依赖于计算机的CPU配置。

Stream 能实现的功能,for 循环都能实现,只是 Stream 代码一般比较简洁,可读性强。但在某些情况下,使用 for 循环要比 Stream 要简洁代码逻辑清晰

举个例子:

1
private List<Order> orderList = Arrays.asList(
2
		new Order(103),
3
		new Order(106),
4
		new Order(107),
5
		new Order(104),
6
		new Order(102),
7
		new Order(103),
8
		new Order(102),
9
		new Order(101),
10
		new Order(104),
11
		new Order(102),
12
		new Order(105)
13
);
14
// 现根据 userId 设置 Order 对象的 name 属性
15
16
// 使用 stream
17
List<Order> newOrderList = orderList.stream()
18
			.map(o -> userList.stream()
19
					.filter(u -> u.getId() == o.getUserId())
20
					.findFirst()
21
					.map(u -> {
22
						o.setUserName(u.getName());
23
						return o;
24
					})
25
					.orElse(o))
26
			.collect(Collectors.toList());
27
newOrderList.stream().forEach(System.out::println);
28
29
// 使用 for 循环
30
   for (Order o : orderList) {
31
       for (User u : userList) {
32
           if (o.getUserId() == u.getId()) {
33
               o.setUserName(u.getName());
34
               break;
35
           }
36
       }
37
   }
38
   orderList.stream().forEach(System.out::println);

在这个例子中,使用 for 循环要比 使用 stream 干净利落的多,代码逻辑清晰简明,可读性也比 stream 好。

千言万语不及官方文档,详情请阅读 compiler:compile 文档

配置 maven 编译插件的 JDK 版本

maven 编译插件(maven-compiler-plugin)有默认编译 JDK 的版本,但这个 JDK 版本通常和我们实际开发使用的不一致。

compiler:compile 文档 有2个参数说明了编译 JDK 版本

<source>

The -source argument for the Java compiler.

NOTE: Since 3.8.0 the default value has changed from 1.5 to 1.6

  • Type: java.lang.String
  • Since: 2.0
  • Required: No
  • User Property: maven.compiler.source
  • Default: 1.6

……

<target>

The -target argument for the Java compiler.

NOTE: Since 3.8.0 the default value has changed from 1.5 to 1.6

  • Type: java.lang.String
  • Since: 2.0
  • Required: No
  • User Property: maven.compiler.target
  • Default: 1.6

如上所述,从 maven-compiler-plugin3.8.0 之后,默认编译 JDK 版本就由 1.5 改为 1.6 了。但是这仍然跟不上 JDK 的更新速度,目前大多数系统都在使用 JDK1.8

注意:User Property 这个说明,下面会用到

可以在 pom.xml 中这样配置,修改 maven 编译JDK版本

1
<build>
2
    <plugins>
3
        <plugin>
4
            <groupId>org.apache.maven.plugins</groupId>
5
            <artifactId>maven-compiler-plugin</artifactId>
6
            <version>3.8.0</version>
7
            <configuration>
8
                <source>1.8</source>
9
                <target>1.8</target>
10
            </configuration>
11
        </plugin>
12
    </plugins>
13
</build>

SpringBoot 是如何配置 maven 编译插件的 JDK 版本的 ?

在 SpringBoot 项目中我们只需要如下配置,即可设定 maven 编译插件的 JDK 版本了

1
<properties>
2
    <java.version>1.8</java.version>
3
</properties>

那么,SpringBoot 是怎么做到的呢?

查看 spring-boot-starter-parent-2.x.x.RELEASE 的 pom 文件

1
   <properties>
2
       ...
3
       <java.version>1.8</java.version>==
4
       ...
5
       <maven.compiler.source>${java.version}</maven.compiler.source>
6
       ...
7
       <maven.compiler.target>${java.version}</maven.compiler.target>
8
   </properties>
9
......
10
<build>
11
	 <pluginManagement>
12
           <plugins>
13
               <plugin>
14
                   <artifactId>maven-compiler-plugin</artifactId>
15
                   <configuration>
16
                       <parameters>true</parameters>
17
                   </configuration>
18
               </plugin>
19
            </plugins>
20
</build>

看来,关键点是这个 <parameters> ,看文档是怎么说的

<parameters>

Set to true to generate metadata for reflection on method parameters.

  • Type: boolean
  • Since: 3.6.2
  • Required: No
  • User Property: maven.compiler.parameters
  • Default: false

英文不好,准确意思翻译不出来。但是结合上面这些证据,连猜带蒙的大概能知道配置 <parameters> 有什么作用了。

maven 编译插件如果配置了 <parameters>true</parameters>,那么插件的配置就可以从用户属性中获取了。具体每个配置使用什么样的属性名称,在文档参数的 User Property 都有明确表示。

比如原先我们要 <source>1.8</source> 这样配置,现在使用 3.6.2 版本以上的 maven 编译插件,就可以在用户属性中 <maven.compiler.source>1.8</maven.compiler.source>

SpringBoot 就是这么配置的! (在 SpringBoot 项目中设置 <java.version> 覆盖掉 spring-boot-starter-parent-2.x.x.RELEASE pom 中的属性)

怎么配置 maven 编译插件的 JDK 版本的

如果使用了 SpringBoot ,那么只需在 pom.xml 如下配置

1
<properties>
2
    <java.version>1.8</java.version>
3
</properties>

如果没有使用 SpringBoot,只是单纯的 maven 项目,那么如下配置(其实就是复制了 SpringBoot 的做法)

1
   <properties>
2
       <java.version>1.8</java.version>
3
       <maven.compiler.source>${java.version}</maven.compiler.source>
4
       <maven.compiler.target>${java.version}</maven.compiler.target>
5
   </properties>
6
7
<build>
8
	 <pluginManagement>
9
           <plugins>
10
               <plugin>
11
                   <artifactId>maven-compiler-plugin</artifactId>
12
                   <configuration>
13
                       <parameters>true</parameters>
14
                   </configuration>
15
               </plugin>
16
            </plugins>
17
       </pluginManagement>
18
</build>

2014年3月18日发布了JavaSE 8

不追求技术的新,追求技术的稳定

本质:Lambda 表达式是一个匿名函数
作用:简化代码,增强代码的表达力

Lambda 语法格式

1
// 格式1:无参无返回值
2
() -> System.out.println("Hello World!");
3
4
// 格式2:有参无返回值
5
(x) -> System.out.println(x);
6
7
// 格式3:有参有返回值
8
(x) -> x * x;
9
10
// 格式4:多参有返回值
11
(x, y) -> x + y;
12
13
// 格式5:函数体包含多条语句
14
(x, y) -> {
15
    System.out.println("加法运算");
16
    return x + y;
17
}

Lambda 表达式中的参数的数据类型可以省略,JVM 编译器能够根据上下文推算出,即“类型推断”

两个例子

1
/** 1. Comparator **/
2
TreeSet<Integer> ts1 = new TreeSet<>(new Comparator<Integer>(){
3
    @Override
4
    public int compare(Integer i1, Integer i2) {
5
        return Integer.compare(i1, i2);
6
    }
7
});
8
// lambda 表达式
9
TreeSet<Integer> ts2 = new TreeSet<>((i1, i2) -> {
10
    return Integer.compare(i1, i2);
11
});
12
// 等同于(使用方法引用还可以再次简化)
13
TreeSet<Integer> ts3 = new TreeSet<>((i1, i2) -> Integer.compare(i1, i2));
14
15
16
/** 2. Runnable */
17
Thread t1 = new Thread(new Runnable(){
18
    @Override
19
    public void run() {
20
        System.out.println("当前线程:" + Thread.currentThread().getName());
21
    }
22
});
23
// lambda 表达式
24
Thread t2 = new Thread(() -> {
25
    System.out.println("当前线程:" + Thread.currentThread().getName());
26
});

函数式接口

!!Lambda 表达式需要函数式接口的支持

函数式接口:接口只有一个抽象方法

可以使用注解 @FunctionalInterface 修饰接口,检查是否是函数式接口

1
// 定义函数式接口
2
@FunctionalInterface
3
public interface Calculator<T> {
4
    public T calculate(T x, T y);
5
}
6
7
// 使用函数式接口
8
Calculator<Integer> calculatorAdd = (x, y) -> x + y;
9
Integer result = calculatorAdd.calculate(3, 5);

Java 内置了四大核心函数式接口:

消费型接口 Consumer<T>:消费一个参数对象

1
@FunctionalInterface
2
public interface Consumer<T> {
3
    void accept(T t);
4
    ...
5
}

供给型接口 Supplier<T>:返回一个对象

1
@FunctionalInterface
2
public interface Supplier<T> {
3
    T get();
4
}

函数型接口 Function<T, R>:传递一个参数,返回一个值

1
@FunctionalInterface
2
public interface Function<T, R> {
3
    R apply(T t);
4
    ...
5
}

断定型接口 Predicate<T>:判断参数是否满足约束

1
@FunctionalInterface
2
public interface Predicate<T> {
3
    boolean test(T t);
4
    ...
5
}

对于Java内置函数式接口建议结合 stream 方法理解,在这里了解即可

除了这4大核心函数式接口外,还有由它们延伸出的一些变种,比如二元消费型接口 BiConsumer<T, U>

1
public interface BiConsumer<T, U> {
2
    void accept(T t, U u);
3
    ...
4
}

方法引用

将 lambda 体代码封装为方法,然后方法引用,再次简化代码。

方法引用格式:类名::方法名对象::方法名

温馨提示:

实际上,在开发工具 IDEA 中,会自动提示使用方法引用简化代码,你只需按 ALT+Eenter 快捷键,根据提示选择操作即可

如果你想要深入了解方法引用的使用原则,可以继续往下看。(即使不看也没大问题,有开发工具帮你优化)

使用方法引用改写 Comparator 例子中的 lambda 表达式

1
// 
2
TreeSet<Integer> ts3 = new TreeSet<>((i1, i2) -> Integer.compare(i1, i2));
3
// 使用方法引用
4
TreeSet<Integer> ts4 = new TreeSet<>(Integer::compare);

(第一种情况)实现函数式接口方法的参数列表,必须和方法引用方法的参数列表保持一致

Comparator.compare(o1, o2) 的 o1, o2 与 Integer.compare(i1, i2) 中的 i1, i2 对应,所以才能够使用方法应用。

当函数式接口方法只有一个参数时(小例子):

1
2
@Test
3
public void test3() {
4
	List<String> stringList = Arrays.asList("北京", "天津", "上海");
5
       // `Consumer.accept(t)` 的参数 t 与 `System.out.println(o)` 的 o 对应
6
	show(System.out::println, stringList);
7
}
8
// 自定义一个函数
9
void show(Consumer<String> consumer, List<String> list) {
10
	for (String s : list) {
11
		consumer.accept(s);
12
	}
13
}

还有第二种情况

1
TreeSet<Integer> ts3 = new TreeSet<>((i1, i2) -> i1.compareTo(i2));
2
// 使用方法引用
3
TreeSet<Integer> ts4 = new TreeSet<>(Integer::compareTo);

Comparator.compare(o1, o2) 的 o1, o2 与 i1.compareTo(i2) 中 i1, i2 对应,这样也能使用方法引用。

(第二种情况)假设函数式接口方法参数有 (x1, x2),而方法实现是 x1.fun(x2) 这种形式,照样使用方法引用

如果理解了它们的规律,推而广之,可以试试抽象方法含有三个参数的情况。

准备工作:找一个三参数的函数式接口

1
@Test
2
public void test4() {
3
	String s = "Hello World";
4
	Integer start = 0;
5
	Integer length = 5;
6
	String r1 = consume((o1, o2, o3) -> {
7
		return o1.substring(o2, o3);
8
	}, s, start, length);
9
	System.out.println(r1);
10
11
	String r2 = consume(String::substring, s, start, length);
12
	System.out.println(r2);
13
}
14
String consume(TripleFunction<String, Integer, Integer, String> tripleFunction, 
15
                                String s, 
16
                                Integer start, 
17
                                Integer length) {
18
	return tripleFunction.apply(s, start, length);
19
}
20
// 自定义三参数函数式接口
21
@FunctionalInterface
22
interface TripleFunction<T, U, E, R> {
23
	R apply(T t, U u, E e);
24
}

这里 函数式接口 TripleFunction 的抽象方法 apply(T t, U u, E e)中的参数 t, u, e 与 s.substring(start, length) 中的 s,start, length 对应

小结:

设函数式接口抽象方法 abstractFun(n1, n2, n3, ..., nn) **
*有方法fun(n1, n2, n3, ..., nn)n1.fun(n2, n3, ..., nn) * 实现了 lambda 体的代码功能
**就可使用方法引用 ClassName::fun

构造器引用

用法:

1
2
Function<Integer, MyClass> fun1 = (i) -> new MyClass(i);
3
// 使用构造器引用
4
Function<Integer, MyClass> fun2 = MyClass::new;
5
6
Function<Integer, Integer[]> fun3 = (n) -> new Integer[n];
7
// 使用构造器引用
8
Function<Integer, Integer[]> fun4 = Integer[]::new;

函数式接口方法参数列表,必须和构造函数参数列表一致 (和方法应用的第一种情况相同)

统一建模语言(Unified Modeling Language,UML)

作用:对软件系统进行说明

如果说软件系统是一本小说的话,那么 UML 就是将小说动漫化。

也许你看过这本小说,但是时间一长,你会忘记一些东西,但是动漫化的故事你不会轻易忘记。

用一张类图说明类之间的关系

类之间的关系

类之间的关系通常有6中:

  • 泛化(Generalization)
  • 实现(Realization)
  • 关联(Association)
  • 聚合(Aggregation)
  • 组合(Composition)
  • 依赖(Dependency)

将上面那张类图放入脑海中!
下面简单用文字简单解释一下类间的关系

泛化 = 继承

比如鸟类是动物的泛化,网络小说是小说的泛化 ….
子类具有父类的特征,但又具有父类所没有的特征。

实现:是由抽象到具体的过程

关联:比如商品销量和价格是有关联关系的,航班和天气是有关联关系的,企鹅和气候是有关联关系的 …

聚合:指多个对象聚在一起形成了一个群体,但这些对象单独又是一个整体

组合:是包含关系,比如鸟类包含翅膀、躯干、脑袋、尾巴等,但不能说翅膀就是鸟类

依赖:人离了氧气就会死,这就是依赖。类的功能依赖另一个类,没有另一个类,这个类的功能就不完整。

!!!一定要记住上面那张图

MySQL 用户

1
--登录
2
mysql -u<用户名> -p[密码] 
3
--修改密码
4
mysqladmin -u<用户名> -p[密码] password <new_password>

数据库

1
--显示所有的数据库
2
show databases;
3
4
--创建数据库,设置字符集utf-8,校对集 utf8_general_ci
5
create database db_name default character set utf8 collate utf8_general_ci;
6
7
--查看创建数据库的语句
8
show create database db_name;

备份和恢复

1
--导出数据库(注意:当前命令是在cmd命令行下执行)
2
mysqldump -u<用户名> -p[密码] db_name > filename
3
4
--恢复
5
--1.先创建一个数据库
6
create database db_name default character set utf8 collate utf8_general_ci;
7
--2.使用该数据库
8
use db_name;
9
--3.导入数据库数据
10
source filename;

表操作

1
--创建表
2
create table student(
3
	id int(11) primary key auto_increment,
4
	name varchar(50) unique not null,
5
	age int,
6
	sex tinyint(1) default 0 comment '0男1女'
7
) ENGINE=INNODB;
8
9
--查看创建表的原始语句
10
show create table table_name;
11
12
--查看表结构
13
desc table_name;
14
15
--查看所有表
16
show tables;
17
18
--删除表
19
drop table table_name;
20
21
--重命名表
22
alter table table_name rename new_table_name;

列操作

1
--添加列
2
alter table table_name add column column_name int not null;
3
4
--删除列
5
alter table table_name drop column column_name;
6
7
--修改列属性
8
alter table table_name modify column column_name float not null;
9
10
--修改列
11
alter table table_name change column column_name_1 column_name2 float default 0;

索引

1
--添加索引
2
alter table table_name add index ind_column_name(column_name);
3
create index ind_column_name on table_name(column_name);
4
5
--删除索引
6
alter table table_name drop index ind_column_name;
7
drop index ind_column_name on table_name(column_name);
8
9
--查看索引
10
show index from table_name;
11
12
-- PS1:索引是不可修改的,只能删除后再创建
13
-- PS2:对表记录的删除会造成索引的存储碎片,过多的存储碎片不仅占用存储空间,还会降低数据库运行速度。重建索引能够有效的进行“碎片整理”。
14
-- 查看索引存储碎片(当 Data_free 列值大于0时表示有碎片,值越大碎片越多)
15
show table status like 'table_name';

增删改查

1
--插入
2
insert into student(id, name, age, sex) values(1, '小明', 23, 0);
3
--批量插入
4
insert into student(id, name, age, sex) values
5
(NULL, '小强', 18, 0),
6
(NULL, '小华', 28, 1),
7
(NULL, '小张', 23, 1);
8
9
--删除
10
delete from student where id=1;
11
12
--更新
13
update student set age=24 where id=2;
14
15
--查询
16
select * from student;

SQL 执行顺序

以下内容摘抄在 《MySQL技术内幕:SQL编程》一书

1
(7)SELECT8DISTINCT <select_list>
2
1FROM <left_table>
3
3)<join_type> JOIN <right_table>
4
2ON <join_condition>
5
4WHERE <where_condition>
6
5GROUP BY <group_by_list>
7
6HAVING <having_condition>
8
9ORDER BY <order_by_list>
9
10LIMIT <limit_number>

(1)FROM:对 FROM 子句中的 执行笛卡儿积,产生虚拟表 VT1
(2)ON:对虚拟表 VT1 应用 ON 筛选,只有符合 的行才被插入虚拟表 VT2中
(3)JOIN:如果指定了 OUTER JOIN (如 LEFT OUTER JOIN、RIGHT OUTER JOIN),那么保留表中未匹配的行作为外部行添加到虚拟表 VT2 中,产生虚拟表 VT3。如果 FROM 子句包含两个以上的表,则对上一个连接生成的结果表 VT3 和下一个表重复执行步骤(1)—步骤(3),直到处理完所有的表为止
(4)WHERE:对虚拟表 VT3 应用 WHERE 过滤条件,只有符合 的记录才被插入虚拟表 VT4 中
(5)GROUP BY:根据 GROUP BY 子句中的列,对 VT4 中的记录进行分组操作,产生 VT5
(6)HAVING:对虚拟表 VT5 应用到 HAVING 过滤器,只有符合 的记录才被插入虚拟表 VT6 中
(7)SELECT:第二次执行 SELECT 操作,选择指定的列,插入到虚拟表 VT7
(8)DISTINCT:去除重复数据,产生虚拟表 VT8
(9)ORDER BY:将虚拟表 VT8 中的记录按照 进行排序操作,产生虚拟表 VT9
(10)LIMIT:取出指定行的记录,产生虚拟表 VT10,并返回给查询用户

原理在上篇已经说过了,实现起来并不难,你可以自己写一个 jwt 工具类(如果你有兴趣的话)

当然了,重复造轮子不是程序员的风格,我们主张拿来主义!
鲁迅的拿来主义

JWT 官网提供了多种语言的 JWT 库,详情可以参考 https://jwt.io/#debugger 页面下半部分

建议使用 jjwt库 ,它的github地址 https://github.com/jwtk/jjwt

jjwt 版本 0.10.7,它和 0.9.x 有很大的区别,一定要注意!!!

本文分5部分

  • 第1部分:以简单例子演示生成、验证、解析 jwt 过程
  • 第2部分:介绍 jjwt 的常用方法
  • 第3部分:封装一个常用的 jwt 工具类
  • 如果只是拿来主义,看到这里就可以了*
  • 第4部分:介绍 jjwt 的各种签名算法
  • 第5部分:对 jwt 进行安全加密

简单例子

引入 MAVN 依赖

1
<dependency>
2
    <groupId>io.jsonwebtoken</groupId>
3
    <artifactId>jjwt-api</artifactId>
4
    <version>0.10.7</version>
5
</dependency>
6
<dependency>
7
    <groupId>io.jsonwebtoken</groupId>
8
    <artifactId>jjwt-impl</artifactId>
9
    <version>0.10.7</version>
10
    <scope>runtime</scope>
11
</dependency>
12
<dependency>
13
    <groupId>io.jsonwebtoken</groupId>
14
    <artifactId>jjwt-jackson</artifactId>
15
    <version>0.10.7</version>
16
    <scope>runtime</scope>
17
</dependency>

一个例子

1
2
// 生成密钥
3
String key = "0123456789_0123456789_0123456789";
4
SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());
5
6
// 1. 生成 token
7
String token = Jwts.builder()     // 创建 JWT 对象
8
        .setSubject("JSON Web Token")   // 设置主题(声明信息)
9
        .signWith(secretKey)    // 设置安全密钥(生成签名所需的密钥和算法)
10
        .compact(); // 生成token(1.编码 Header 和 Payload 2.生成签名 3.拼接字符串)
11
System.out.println(token);
12
13
//token = token + "s";
14
15
// 2. 验证token,如果验证token失败则会抛出异常
16
try {
17
    Jwts.parser()
18
            .setSigningKey(secretKey)
19
            .parseClaimsJws(token);
20
    // OK, we can trust this token
21
    System.out.println("验证成功");
22
} catch (JwtException e) {
23
    //don't trust the token!
24
    System.out.println("验证失败");
25
}
26
27
// 3. 解析token
28
Claims body = Jwts.parser()     // 创建解析对象
29
        .setSigningKey(secretKey)   // 设置安全密钥(生成签名所需的密钥和算法)
30
        .parseClaimsJws(token)  // 解析token
31
        .getBody(); // 获取 payload 部分内容
32
System.out.println(body);

输出结果:

1
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKU09OIFdlYiBUb2tlbiJ9.QwmY_0qXW4BhAHcDpxz62v3xqkFYbg5lsZQhM2t-kVs
2
验证成功
3
{sub=JSON Web Token}

常用方法

以下内容建议参考源码获知更多详情

Jwts.builder() 创建了 DefaultJwtBuilder 对象,该对象的常用方法如下:

compact() 方法中会自动根据签名算法设置头部信息,当然也可以手动设置

  • setHeader(Header header): JwtBuilder
  • setHeader(Map<String, Object> header): JwtBuilder
  • setHeaderParams(Map<String, Object> params): JwtBuilder
  • setHeaderParam(String name, Object value): JwtBuilder

    参数 Header 对象 可通过 Jwts.header(); 创建,它简单得就像一个 map (把它当做 map 使用即可)

Payload

至少设置一个 claims,否则在生成签名时会抛出异常

  • setClaims(Claims claims): JwtBuilder
  • setClaims(Map<String, Object> claims): JwtBuilder
  • addClaims(Map<String, Object> claims): JwtBuilder
  • setIssuer(String iss): JwtBuilder
  • setSubject(String sub): JwtBuilder
  • setAudience(String aud): JwtBuilder
  • setExpiration(Date exp): JwtBuilder
  • setNotBefore(Date nbf): JwtBuilder
  • setIssuedAt(Date iat): JwtBuilder
  • setId(String jti): JwtBuilder
  • claim(String name, Object value: JwtBuilder

参数对象 Claims 同 Header 类似,通过 Jwts.claims() 创建,同样简单得就像一个 map

值得注意的一点是:不要在 setXxx 之后调用 setClaims(Claims claims) 或 setClaims(Map<String, Object> claims),因为这两个方法会覆盖所有已设置的 claim

Signature

  • signWith(Key key)
  • signWith(Key key, SignatureAlgorithm alg)
  • signWith(SignatureAlgorithm alg, byte[] secretKeyBytes)
  • signWith(SignatureAlgorithm alg, String base64EncodedSecretKey)
  • signWith(SignatureAlgorithm alg, Key key)

以上方法最终就是设置两个对象:key 和 algorithm,分别代表密钥和算法
方法内部生成密钥使用的方法的和演示中的一样

1
SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());

注意:key 的长度必须符合签名算法的要求(避免生成弱密钥)
HS256:bit 长度要>=256,即字节长度>=32
HS384:bit 长度要>=384,即字节长度>=48
HS512:bit 长度要>=512,即字节长度>=64
在 secret key algorithms 名称中的数字代表了最小bit长度

更多签名算法的详情,请参考签名算法小节

封装 JWT 工具类

1
package com.liuchuanv.jwt;
2
3
import io.jsonwebtoken.*;
4
import io.jsonwebtoken.security.SignatureException;
5
6
import javax.crypto.spec.SecretKeySpec;
7
import java.security.Key;
8
import java.util.Date;
9
import java.util.Map;
10
import java.util.UUID;
11
12
/**
13
 * JSON Web Token 工具类
14
 *
15
 * @author LiuChuanWei
16
 * @date 2019-12-11
17
 */
18
public class JwtUtils {
19
20
    /**
21
     * key(按照签名算法的字节长度设置key)
22
     */
23
    private final static String SECRET_KEY = "0123456789_0123456789_0123456789";
24
    /**
25
     * 过期时间(毫秒单位)
26
     */
27
    private final static long TOKEN_EXPIRE_MILLIS = 1000 * 60 * 60;
28
29
    /**
30
     * 创建token
31
     * @param claimMap
32
     * @return
33
     */
34
    public static String createToken(Map<String, Object> claimMap) {
35
        long currentTimeMillis = System.currentTimeMillis();
36
        return Jwts.builder()
37
                .setId(UUID.randomUUID().toString())
38
                .setIssuedAt(new Date(currentTimeMillis))    // 设置签发时间
39
                .setExpiration(new Date(currentTimeMillis + TOKEN_EXPIRE_MILLIS))   // 设置过期时间
40
                .addClaims(claimMap)
41
                .signWith(generateKey())
42
                .compact();
43
    }
44
45
    /**
46
     * 验证token
47
     * @param token
48
     * @return 0 验证成功,1、2、3、4、5 验证失败
49
     */
50
    public static int verifyToken(String token) {
51
        try {
52
            Jwts.parser().setSigningKey(generateKey()).parseClaimsJws(token);
53
            return 0;
54
        } catch (ExpiredJwtException e) {
55
            e.printStackTrace();
56
            return 1;
57
        } catch (UnsupportedJwtException e) {
58
            e.printStackTrace();
59
            return 2;
60
        } catch (MalformedJwtException e) {
61
            e.printStackTrace();
62
            return 3;
63
        } catch (SignatureException e) {
64
            e.printStackTrace();
65
            return 4;
66
        } catch (IllegalArgumentException e) {
67
            e.printStackTrace();
68
            return 5;
69
        }
70
    }
71
72
    /**
73
     * 解析token
74
     * @param token
75
     * @return
76
     */
77
    public static Map<String, Object> parseToken(String token) {
78
        return Jwts.parser()  // 得到DefaultJwtParser
79
                .setSigningKey(generateKey()) // 设置签名密钥
80
                .parseClaimsJws(token)
81
                .getBody();
82
    }
83
84
    /**
85
     * 生成安全密钥
86
     * @return
87
     */
88
    public static Key generateKey() {
89
       return new SecretKeySpec(SECRET_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName());
90
    }
91
}

测试代码如下:

1
//Map<String, Object> map = new HashMap<String, Object>();
2
      //map.put("userId", 1002);
3
      //map.put("userName", "张晓明");
4
      //map.put("age", 12);
5
      //map.put("address", "山东省青岛市李沧区");
6
      //String token = JwtUtils.createToken(map);
7
      //System.out.println(token);
8
9
      String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ZWM2NWNhNC0wZjVmLTRlOTktOTI5NS1mYWUyN2UwODIzYzQiLCJpYXQiOjE1NzY0OTI4NjYsImV4cCI6MTU3NjQ5NjQ2NiwiYWRkcmVzcyI6IuWxseS4nOecgemdkuWym-W4guadjuayp-WMuiIsInVzZXJOYW1lIjoi5byg5pmT5piOIiwidXNlcklkIjoxMDAyLCJhZ2UiOjEyfQ.6Z18aIA6y52ntQkV3BwlYiVK3hL3R2WFujjTmuvimww";
10
      int result = JwtUtils.verifyToken(token);
11
      System.out.println(result);
12
13
      Map<String, Object> map = JwtUtils.parseToken(token);
14
      System.out.println(map);

输出结果:

1
0
2
{jti=4ec65ca4-0f5f-4e99-9295-fae27e0823c4, iat=1576492866, exp=1576496466, address=山东省青岛市李沧区, userName=张晓明, userId=1002, age=12}

签名算法

12 种签名算法

JWT 规范定义了12种标准签名算法:3种 secret key 算法和9种非对称密钥算法

  • HS256: HMAC using SHA-256
  • HS384: HMAC using SHA-384
  • HS512: HMAC using SHA-512
  • ES256: ECDSA using P-256 and SHA-256
  • ES384: ECDSA using P-384 and SHA-384
  • ES512: ECDSA using P-521 and SHA-512
  • RS256: RSASSA-PKCS-v1_5 using SHA-256
  • RS384: RSASSA-PKCS-v1_5 using SHA-384
  • RS512: RSASSA-PKCS-v1_5 using SHA-512
  • PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
  • PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
  • PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512

根据算法名称可分为四类:HSxxx(secret key 算法)、ESxxx、RSxxx、PSxxx

HSxxx、ESxxx 中的 xxx 表示算法 key 最小 Bit 长度
RSxxx、PSxxx 中的 xxx 表示算法 key 最小 Byte 长度

规定key的最小长度是为了避免因 key 过短生成弱密钥

生成密钥

jjwt 生成 secret key 两种方法

1
String key = "1234567890_1234567890_1234567890";
2
// 1. 根据key生成密钥(会根据字节参数长度自动选择相应的 HMAC 算法)
3
SecretKey secretKey1 = Keys.hmacShaKeyFor(key.getBytes());
4
// 2. 根据随机数生成密钥
5
SecretKey secretKey2 = Keys.secretKeyFor(SignatureAlgorithm.HS256);

方法 Keys.hmacShaKeyFor(byte[]) 内部也是 new SecretKeySpec(bytes, alg.getJcaName()) 来生成密钥的
方法 Keys.secretKeyFor(SignatureAlgorithm) 内部使用 KeyGenerator.generateKey() 生成密钥

jjwt 也提供了非对称密钥对的生成方法

1
// 1. 使用jjwt提供的方法生成
2
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);    //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
3
4
// 2. 手动生成
5
int keySize = 1024;
6
// RSA算法要求有一个可信任的随机数源
7
SecureRandom secureRandom = new SecureRandom();
8
// 为RSA算法创建一个KeyPairGenerator对象 
9
KeyPairGenerator keyPairGenerator = null;
10
try {
11
    keyPairGenerator = KeyPairGenerator.getInstance("RSA");
12
} catch (NoSuchAlgorithmException e) {
13
    e.printStackTrace();
14
}
15
// 利用上面的随机数据源初始化这个KeyPairGenerator对象
16
keyPairGenerator.initialize(keySize, secureRandom);
17
// 生成密钥对
18
KeyPair keyPair2 = keyPairGenerator.generateKeyPair();
  • Keys.keyPairFor(SignatureAlgorithm) 会根据算法自动生成相应长度的
  • signWith(secretKey) 会根据密钥长度自动选择相应算法,也可以指定任意算法(指定的算法不受密钥长度限制,可任意选择,即用 RS256生成的密钥,可以 signWith(secretKey, SignatureAlgorithm.RS512),但是 JJWT 并不建议这么做)
  • 在加密时使用 keyPair.getPrivate() ,解密时使用 keyPair.getPublic()

不同密钥生成token

以上都是使用同一密钥签名生成所有的token,下面我们使用不同的密钥

这一个特性可以应用于不同用户/角色使用不同的密钥生成的 token,帮助你更好的构建权限系统

  1. 首先在 Header(或 claims)中设置一个 keyId

  2. 定义一个类,继承 SigningKeyResolverAdapter,并重写 resolveSigningKey() 或 resolveSigningKeyBytes() 方法

    1
    public class MySigningKeyResolver extends SigningKeyResolverAdapter {
    2
        @Override
    3
        public Key resolveSigningKey(JwsHeader header, Claims claims) {
    4
            // 除了从 header 中获取 keyId 外,也可以从 claims 中获取(前提是在 claims 中设置了 keyId 声明)
    5
            String keyId = header.getKeyId();
    6
            // 根据 keyId 查找相应的 key
    7
            Key key = lookupVerificationKey(keyId);
    8
            return key;
    9
        }
    10
    11
        public Key lookupVerificationKey(String keyId) {
    12
            // TODO 根据 keyId 获取 key,比如从数据库中获取
    13
            // 下面语句仅做演示用,绝对不可用于实际开发中!!!
    14
            String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
    15
            return Keys.hmacShaKeyFor(key.getBytes());
    16
        }
    17
    }
  3. 解析时,不再调用 setSigningKey(SecretKey) ,而是调用 setSigningKeyResolver(SigningKeyResolver)

    1
    // 生成密钥
    2
            // TODO 此处 keyId 仅做演示用,实际开发中可以使用 UserId、RoleId 等作为 keyId
    3
            String keyId = new Long(System.currentTimeMillis()).toString();
    4
            System.out.println("keyId=" + keyId);
    5
    6
            String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
    7
            SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());
    8
    9
            // 1. 生成 token
    10
            String token = Jwts.builder()
    11
                    .setHeaderParam(JwsHeader.KEY_ID, keyId)    // 设置 keyId(当然也可以在 claims 中设置)
    12
                    .setSubject("JSON Web Token")
    13
                    .signWith(secretKey)
    14
                    .compact();
    15
            System.out.println("token=" + token);
    16
    17
            // 2. 验证token
    18
            // token 使用了不同的密钥生成签名,在解析时就不用调用 setSigningKey(SecretKey) 了
    19
            // 而是调用 setSigningKeyResolver(SigningKeyResolver)
    20
            try {
    21
                Jwts.parser()
    22
                        .setSigningKeyResolver(new MySigningKeyResolver())
    23
                        .parseClaimsJws(token);
    24
                // OK, we can trust this token
    25
                System.out.println("token验证成功");
    26
            } catch (JwtException e) {
    27
                //don't trust the token!
    28
                System.out.println("token验证失败");
    29
            }

安全加密

敬请期待 …..

什么是 JWT?

JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,作为 JSON 对象在各方之间安全的传递信息。这个信息可以通过数字签名进行验证并信任。JWTs 可以使用密钥(结合 HMAC 算法)或者 使用 RSA 、 ECDSA 加密的公钥私钥对进行签名。

尽管 JWTs 能够在各方之间也提供安全加密,但是仍*专注于签名 Token *。当其他方隐藏了加密 Token 的某些声明时,签名 Token 可以验证声明的完整性。当Token使用公钥私钥对进行签名时,这个签名还能证明只有拥有私钥的一方才是签发它的一方。

什么时候使用 JWT ?

  • 授权:这是 JWT 最普遍的使用场景。当用户登录之后,每次请求中都包含 JWT ,服务端允许用户访问那些只有携带 token 才能访问的路由、服务、资源。目前在单点登录中广泛使用到 JWT ,因为它体积小,且能够在不同域名之间使用。
  • 信息交换: JWT 是一种能够在各方之间安全传输信息的好方式。因为 JWTs 能够签名,比如使用公钥私钥对,你能够确定发送者的身份。另外,签名是使用 Header 和 Payload 通过特定算法计算而来,所以你也可以验证内容是否被篡改。

JWT的结构

JWT 包含三部分,之间以点(.)连接

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

一个典型的 JWT 如同下面这样:

1
xxxxx.yyyyy.zzzzz

一个真实的 JWT 例子:

编码的JWT

Header部分 是一个 JSON 对象,典型的header包含两部分:

  • alg:使用的签名算法,比如 HMAC SHA256 或 RSA
  • typ:token的类型,比如 JWT
1
{
2
  "alg": "HS256",
3
  "typ": "JWT"
4
}

最后,用 Base64Url 将这个 JSON 对象编码后,作为 JWT 的第一部分

Payload

Payload 部分也是 JSON 对象,用来存放数据。JWT 有7个官方字段:

  • iss (issuer):签发人
  • exp (expiration time):过期时间,以秒为单位
  • iat (Issued At):签发时间,能够算出JWT的存在时间
  • nbf (Not Before):生效时间
  • jti (JWT ID):JWT 的唯一标识。用来防止 JWT 重复。
  • sub (subject):主题(很少使用)
  • aud (audience):token的受众(很少被使用)

除了上面这些字段,还可以自定义私有字段,比如

1
{
2
	"userId": "1101",
3
	"userName": "张三",
4
	"age": "23"
5
}

最后,用 Base64Url 将这个 JSON 对象编码后,作为 JWT 的第二部分

Tip: JWT 默认不加密,任何人都可以读取,所以不要把敏感信息存放在这个部分,除非加密过。

Signature

使用 Header 指定的算法对 Header、Payload、密钥三部分进行签名,生成的字符串作为 JWT 的第三部分。
比如使用 HMAC SHA256 算法进行签名:

1
HMACSHA256( 
2
	Base64Url.encode(header) + "." + Base64Url.encode(payload),
3
	secret
4
)

签名可以用来验证数据是否被篡改,而且如果 token 使用私钥进行了签名,那么该签名还可以验证 JWT 发送者的身份。

怎么使用 JWT ?

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次请求服务器,都要带上这个 JWT。所以可以把它放在 Cookie 里面自动发送,但是这样并不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

1
Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT 的特点

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
  2. JWT 不加密的情况下,不能将敏感数据写入 JWT
  3. JWT 不仅可以用于认证,也可以用于交换信息
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦签发了 JWT ,在到期之前就会始终有效
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

Session-Cookie 方式:客户端每次请求都使用 cookie 携带 session_id ,服务器根据 session_id 区分不同的会话

JWT 方式:客户端每次请求都使用请求头携带 token,服务器根据 token 区分不同的用户

官网 JWT 介绍

注解 InitBinder 是用来初始化绑定器Binder的,而Binder是用来绑定数据的,换句话说就是将请求参数转成数据对象。

@InitBinder用于在@Controller中标注于方法,表示为当前控制器注册一个属性编辑器或者其他,只对当前的Controller有效。

@InitBinder 有2个基本用途,类型转换参数绑定

类型转换

比如,将“2019-12-06 16:59:59”这样的字符串转成 java.util.Date 对象

1
@InitBinder
2
public void initBinder(WebDataBinder binder) {
3
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
4
    dateFormat.setLenient(false);
5
    binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
6
}

参数绑定

比如,html表单是下面这样的

1
<form action="/buy" method="post">
2
    name: <input type="text" name="customer.name"> <br>
3
    age: <input type="text" name="customer.customerId"> <br>
4
    name: <input type="text" name="goods.title"> <br>
5
    age: <input type="text" name="goods.price"> <br>
6
    <input type="submit">
7
</form>

在后台将以customer为前缀的参数绑定到Customer对象上,将以goods为前缀的参数绑定到Goods对象上

1
@InitBinder("customer")
2
public void initCustomer(WebDataBinder binder) {
3
    binder.setFieldDefaultPrefix("customer.");
4
}
5
 
6
@InitBinder("goods")
7
public void initGoods(WebDataBinder binder) {
8
    binder.setFieldDefaultPrefix("goods.");
9
}
10
11
@PostMapping("/buy")
12
public ModelAndView buy(Customer customer, @ModelAttribute("goods") Goods goods, ModelAndView mv) {
13
    // do something
14
    return mv;
15
 
16
}

@ModelAttribute(“goods”) 中的 “goods” 用来指定 @InitBinder(“goods”)

换句话讲,先在 initGoods 方法中,将以 goods 为前缀的参数封装为名为 goods 的对象;然后在 buy 方法中使用 @ModelAttribute(“goods”) 来接收名为 goods 的对象。