大家好,又见面了。

在我前面的文章《吃透JAVA的Stream流操作,多年实践总结》中呢,对Stream的整体情况进行了细致全面的讲解,也大概介绍了下结果收集器Collectors的常见用法 —— 但远不是全部。

本篇文章就来专门剖析collect操作,一起解锁更多高级玩法,让Stream操作真正的成为我们编码中的神兵利器

初识Collector

先看一个简单的场景:

现有集团内所有人员列表,需要从中筛选出上海子公司的全部人员

假定人员信息数据如下:

姓名 子公司 部门 年龄 工资
大壮 上海公司 研发一部 28 3000
二牛 上海公司 研发一部 24 2000
铁柱 上海公司 研发二部 34 5000
翠花 南京公司 测试一部 27 3000
玲玲 南京公司 测试二部 31 4000

如果你曾经用过Stream流,或者你看过我前面关于Stream用法介绍的文章,那么借助Stream可以很轻松的实现上述诉求:

public void filterEmployeesByCompany() {
List<Employee> employees = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(Collectors.toList());
System.out.println(employees);
}

上述代码中,先创建流,然后通过一系列中间流操作(filter方法)进行业务层面的处理,然后经由终止操作(collect方法)将处理后的结果输出为List对象。

但我们实际面对的需求场景中,往往会有一些更复杂的诉求,比如说:

现有集团内所有人员列表,需要从中筛选出上海子公司的全部人员,并按照部门进行分组

其实也就是加了个新的分组诉求,那就是先按照前面的代码实现逻辑基础上,再对结果进行分组处理就好咯:

public void filterEmployeesThenGroup() {
// 先 筛选
List<Employee> employees = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(Collectors.toList());
// 再 分组
Map<String, List<Employee>> resultMap = new HashMap<>();
for (Employee employee : employees) {
List<Employee> groupList = resultMap
.computeIfAbsent(employee.getDepartment(), k -> new ArrayList<>());
groupList.add(employee);
}
System.out.println(resultMap);
}

似乎也没啥毛病,相信很多同学实际编码中也是这么处理的。但其实我们也可以使用Stream操作直接完成:

public void filterEmployeesThenGroupByStream() {
Map<String, List<Employee>> resultMap = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println(resultMap);
}

两种写法都可以得到相同的结果:

{
研发二部=[Employee(subCompany=上海公司, department=研发二部, name=铁柱, age=34, salary=5000)],
研发一部=[Employee(subCompany=上海公司, department=研发一部, name=大壮, age=28, salary=3000),
Employee(subCompany=上海公司, department=研发一部, name=二牛, age=24, salary=2000)]
}

上述2种写法相比而言,第二种是不是代码上要简洁很多?而且是不是有种自注释的味道了?

通过collect方法的合理恰当利用,可以让Stream适应更多实际的使用场景,大大的提升我们的开发编码效率。下面就一起来全面认识下collect、解锁更多高级操作吧。

collect\Collector\Collectors区别与关联

刚接触Stream收集器的时候,很多同学都会被collect,Collector,Collectors这几个概念搞的晕头转向,甚至还有很多人即使已经使用Stream好多年,也只是知道collect里面需要传入类似Collectors.toList()这种简单的用法,对其背后的细节也不甚了解。

这里以一个collect收集器最简单的使用场景来剖析说明下其中的关系:

概括来说

1️⃣ collect是Stream流的一个终止方法,会使用传入的收集器(入参)对结果执行相关的操作,这个收集器必须是Collector接口的某个具体实现类

2️⃣ Collector是一个接口,collect方法的收集器是Collector接口的具体实现类

3️⃣ Collectors是一个工具类,提供了很多的静态工厂方法,提供了很多Collector接口的具体实现类,是为了方便程序员使用而预置的一些较为通用的收集器(如果不使用Collectors类,而是自己去实现Collector接口,也可以)。

Collector使用与剖析

到这里我们可以看出,Stream结果收集操作的本质,其实就是将Stream中的元素通过收集器定义的函数处理逻辑进行加工,然后输出加工后的结果

根据其执行的操作类型来划分,又可将收集器分为几种不同的大类

下面分别阐述下。

恒等处理Collector

所谓恒等处理,指的就是Stream的元素在经过Collector函数处理前后完全不变,例如toList()操作,只是最终将结果从Stream中取出放入到List对象中,并没有对元素本身做任何的更改处理:

恒等处理类型的Collector是实际编码中最常被使用的一种,比如:

list.stream().collect(Collectors.toList());
list.stream().collect(Collectors.toSet());
list.stream().collect(Collectors.toCollection());

归约汇总Collector

对于归约汇总类的操作,Stream流中的元素逐个遍历,进入到Collector处理函数中,然后会与上一个元素的处理结果进行合并处理,并得到一个新的结果,以此类推,直到遍历完成后,输出最终的结果。比如Collectors.summingInt()方法的处理逻辑如下:

比如本文开头举的例子,如果需要计算上海子公司每个月需要支付的员工总工资,使用Collectors.summingInt()可以这么实现:

public void calculateSum() {
Integer salarySum = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(Collectors.summingInt(Employee::getSalary));
System.out.println(salarySum);
}

需要注意的是,这里的汇总计算不单单只数学层面的累加汇总,而是一个广义上的汇总概念,即将多个元素进行处理操作,最终生成1个结果的操作,比如计算Stream中最大值的操作,最终也是多个元素中,最终得到一个结果:

还是用之前举的例子,现在需要知道上海子公司里面工资最高的员工信息,我们可以这么实现:

public void findHighestSalaryEmployee() {
Optional<Employee> highestSalaryEmployee = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));
System.out.println(highestSalaryEmployee.get());
}

因为这里我们要演示collect的用法,所以用了上述的写法。实际的时候JDK为了方便使用,也提供了上述逻辑的简化封装,我们可以直接使用max()方法来简化,即上述代码与下面的写法等价:

public void findHighestSalaryEmployee2() {
Optional<Employee> highestSalaryEmployee = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.max(Comparator.comparingInt(Employee::getSalary));
System.out.println(highestSalaryEmployee.get());
}

分组分区Collector

Collectors工具类中提供了groupingBy方法用来得到一个分组操作Collector,其内部处理逻辑可以参见下图的说明:

groupingBy()操作需要指定两个关键输入,即分组函数值收集器

  • 分组函数:一个处理函数,用于基于指定的元素进行处理,返回一个用于分组的值(即分组结果HashMap的Key值),对于经过此函数处理后返回值相同的元素,将被分配到同一个组里。

  • 值收集器:对于分组后的数据元素的进一步处理转换逻辑,此处还是一个常规的Collector收集器,和collect()方法中传入的收集器完全等同(可以想想俄罗斯套娃,一个概念)。

对于groupingBy分组操作而言,分组函数值收集器二者必不可少。为了方便使用,在Collectors工具类中,提供了两个groupingBy重载实现,其中有一个方法只需要传入一个分组函数即可,这是因为其默认使用了toList()作为值收集器:

例如:仅仅是做一个常规的数据分组操作时,可以仅传入一个分组函数即可:

public void groupBySubCompany() {
// 按照子公司维度将员工分组
Map<String, List<Employee>> resultMap =
getAllEmployees().stream()
.collect(Collectors.groupingBy(Employee::getSubCompany));
System.out.println(resultMap);
}

这样collect返回的结果,就是一个HashMap,其每一个HashValue的值为一个List类型

而如果不仅需要分组,还需要对分组后的数据进行处理的时候,则需要同时给定分组函数以及值收集器:

public void groupAndCaculate() {
// 按照子公司分组,并统计每个子公司的员工数
Map<String, Long> resultMap = getAllEmployees().stream()
.collect(Collectors.groupingBy(Employee::getSubCompany,
Collectors.counting()));
System.out.println(resultMap);
}

这样就同时实现了分组与组内数据的处理操作:

{南京公司=2, 上海公司=3}

上面的代码中Collectors.groupingBy()是一个分组Collector,而其内又传入了一个归约汇总Collector Collectors.counting(),也就是一个收集器中嵌套了另一个收集器。

除了上述演示的场景外,还有一种特殊的分组操作,其分组的key类型仅为布尔值,这种情况,我们也可以通过Collectors.partitioningBy()提供的分区收集器来实现。

例如:

统计上海公司和非上海公司的员工总数, true表示是上海公司,false表示非上海公司

使用分区收集器的方式,可以这么实现:

public void partitionByCompanyAndDepartment() {
Map<Boolean, Long> resultMap = getAllEmployees().stream()
.collect(Collectors.partitioningBy(e -> "上海公司".equals(e.getSubCompany()),
Collectors.counting()));
System.out.println(resultMap);
}

结果如下:

{false=2, true=3}

Collectors.partitioningBy()分区收集器的使用方式与Collectors.groupingBy()分组收集器的使用方式相同。单纯从使用维度来看,分组收集器的分组函数返回值为布尔值,则效果等同于一个分区收集器

Collector的叠加嵌套

有的时候,我们需要根据先根据某个维度进行分组后,再根据第二维度进一步的分组,然后再对分组后的结果进一步的处理操作,这种场景里面,我们就可以通过Collector收集器的叠加嵌套使用来实现。

例如下面的需求:

现有整个集团全体员工的列表,需要统计各子公司内各部门下的员工人数。

使用Stream的嵌套Collector,我们可以这么实现:

public void groupByCompanyAndDepartment() {
// 按照子公司+部门双层维度,统计各个部门内的人员数
Map<String, Map<String, Long>> resultMap = getAllEmployees().stream()
.collect(Collectors.groupingBy(Employee::getSubCompany,
Collectors.groupingBy(Employee::getDepartment,
Collectors.counting())));
System.out.println(resultMap);
}

可以看下输出结果,达到了需求预期的诉求:

{
南京公司={
测试二部=1,
测试一部=1},
上海公司={
研发二部=1,
研发一部=2}
}

上面的代码中,就是一个典型的Collector嵌套处理的例子,同时也是一个典型的多级分组的实现逻辑。对代码的整体处理过程进行剖析,大致逻辑如下:

借助多个Collector嵌套使用,可以让我们解锁很多复杂场景处理能力。你可以将这个操作想象为一个套娃操作,如果愿意,你可以无限嵌套下去(实际中不太可能会有如此荒诞的场景)。

Collectors提供的收集器

为了方便程序员使用呢,JDK中的Collectors工具类封装提供了很多现成的Collector实现类,可供编码时直接使用,对常用的收集器介绍如下:

方法 含义说明
toList 将流中的元素收集到一个List中
toSet 将流中的元素收集到一个Set中
toCollection 将流中的元素收集到一个Collection中
toMap 将流中的元素映射收集到一个Map中
counting 统计流中的元素个数
summingInt 计算流中指定int字段的累加总和。针对不同类型的数字类型,有不同的方法,比如summingDouble等
averagingInt 计算流中指定int字段的平均值。针对不同类型的数字类型,有不同的方法,比如averagingLong等
joining 将流中所有元素(或者元素的指定字段)字符串值进行拼接,可以指定拼接连接符,或者首尾拼接字符
maxBy 根据给定的比较器,选择出值最大的元素
minBy 根据给定的比较器,选择出值最小的元素
groupingBy 根据给定的分组函数的值进行分组,输出一个Map对象
partitioningBy 根据给定的分区函数的值进行分区,输出一个Map对象,且key始终为布尔值类型
collectingAndThen 包裹另一个收集器,对其结果进行二次加工转换
reducing 从给定的初始值开始,将元素进行逐个的处理,最终将所有元素计算为最终的1个值输出

上述的大部分方法,前面都有使用示例,这里对collectAndThen补充介绍下。

collectAndThen对应的收集器,必须传入一个真正用于结果收集处理的实际收集器downstream以及一个finisher方法,当downstream收集器计算出结果后,使用finisher方法对结果进行二次处理,并将处理结果作为最终结果返回。

还是拿之前的例子来举例:

给定集团所有员工列表,找出上海公司中工资最高的员工。

我们可以写出如下代码:

public void findHighestSalaryEmployee() {
Optional<Employee> highestSalaryEmployee = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));
System.out.println(highestSalaryEmployee.get());
}

但是这个结果最终输出的是个Optional<Employee>类型,使用的时候比较麻烦,那能不能直接返回我们需要的Employee类型呢?这里就可以借助collectAndThen来实现:

public void testCollectAndThen() {
Employee employeeResult = getAllEmployees().stream()
.filter(employee -> "上海公司".equals(employee.getSubCompany()))
.collect(
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)),
Optional::get)
);
System.out.println(employeeResult);
}

这样就可以啦,是不是超简单的?

开发个自定义收集器

前面我们演示了很多Collectors工具类中提供的收集器的用法,上一节中列出来的Collectors提供的常用收集器,也可以覆盖大部分场景的开发诉求了。

但也许在项目中,我们会遇到一些定制化的场景,现有的收集器无法满足我们的诉求,这个时候,我们也可以自己来实现定制化的收集器

Collector接口介绍

我们知道,所谓的收集器,其实就是一个Collector接口的具体实现类。所以如果想要定制自己的收集器,首先要先了解Collector接口到底有哪些方法需要我们去实现,以及各个方法的作用与用途。

当我们新建一个MyCollector类并声明实现Collector接口的时候,会发现需要我们实现5个接口:

这5个接口的含义说明归纳如下:

接口名称 功能含义说明
supplier 创建新的结果容器,可以是一个容器,也可以是一个累加器实例,总之是用来存储结果数据的
accumlator 元素进入收集器中的具体处理操作
finisher 当所有元素都处理完成后,在返回结果前的对结果的最终处理操作,当然也可以选择不做任何处理,直接返回
combiner 各个子流的处理结果最终如何合并到一起去,比如并行流处理场景,元素会被切分为好多个分片进行并行处理,最终各个分片的数据需要合并为一个整体结果,即通过此方法来指定子结果的合并逻辑
characteristics 对此收集器处理行为的补充描述,比如此收集器是否允许并行流中处理,是否finisher方法必须要有等等,此处返回一个Set集合,里面的候选值是固定的几个可选项。

对于characteristics返回set集合中的可选值,说明如下:

取值 含义说明
UNORDERED 声明此收集器的汇总归约结果与Stream流元素遍历顺序无关,不受元素处理顺序影响
CONCURRENT 声明此收集器可以多个线程并行处理,允许并行流中进行处理
IDENTITY_FINISH 声明此收集器的finisher方法是一个恒等操作,可以跳过

现在,我们知道了这5个接口方法各自的含义与用途了,那么作为一个Collector收集器,这几个接口之间是如何配合处理并将Stream数据收集为需要的输出结果的呢?下面这张图可以清晰的阐述这一过程:

当然,如果我们的Collector是支持在并行流中使用的,则其处理过程会稍有不同:

为了对上述方法有个直观的理解,我们可以看下Collectors.toList()这个收集器的实现源码:

static final Set<Collector.Characteristics> CH_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

对上述代码拆解分析如下:

  • supplier方法ArrayList::new,即new了个ArrayList作为结果存储容器。

  • accumulator方法List::add,也就是对于stream中的每个元素,都调用list.add()方法添加到结果容器追踪。

  • combiner方法(left, right) -> { left.addAll(right); return left; },也就是对于并行操作生成的各个子ArrayList结果,最终通过list.addAll()方法合并为最终结果。

  • finisher方法:没提供,使用的默认的,因为无需做任何处理,属于恒等操作。

  • characteristics:返回的是IDENTITY_FINISH,也即最终结果直接返回,无需finisher方法去二次加工。注意这里没有声明CONCURRENT,因为ArrayList是个非线程安全的容器,所以这个收集器是不支持在并发过程中使用

通过上面的逐个方法描述,再联想下Collectors.toList()的具体表现,想必对各个接口方法的含义应该有了比较直观的理解了吧?

实现Collector接口

既然已经搞清楚Collector接口中的主要方法作用,那就可以开始动手写自己的收集器啦。新建一个class类,然后声明实现Collector接口,然后去实现具体的接口方法就行咯。

前面介绍过,Collectors.summingInt收集器是用来计算每个元素中某个int类型字段的总和的,假设我们需要一个新的累加功能:

计算流中每个元素的某个int字段值平方的总和

下面,我们就一起来自定义一个收集器来实现此功能。

  • supplier方法

supplier方法的职责,是创建一个结果存储累加的容器。既然我们要计算多个值的累加结果,那首先就是要先声明一个int sum = 0用来存储累加结果。但是为了让我们的收集器可以支持在并发模式下使用,我们这里可以采用线程安全的AtomicInteger来实现。

所以我们便可以确定supplier方法的实现逻辑了:

@Override
public Supplier<AtomicInteger> supplier() {
// 指定用于最终结果的收集,此处返回new AtomicInteger(0),后续在此基础上累加
return () -> new AtomicInteger(0);
}
  • accumulator方法

accumulator方法是实现具体的计算逻辑的,也是整个Collector的核心业务逻辑所在的方法。收集器处理的时候,Stream流中的元素会逐个进入到Collector中,然后由accumulator方法来进行逐个计算:

@Override
public BiConsumer<AtomicInteger, T> accumulator() {
// 每个元素进入的时候的遍历策略,当前元素值的平方与sum结果进行累加
return (sum, current) -> {
int intValue = mapper.applyAsInt(current);
sum.addAndGet(intValue * intValue);
};
}

这里也补充说下,收集器中的几个方法中,仅有accumulator是需要重复执行的,有几个元素就会执行几次,其余的方法都不会直接与Stream中的元素打交道。

  • combiner方法

因为我们前面supplier方法中使用了线程安全的AtomicInteger作为结果容器,所以其支持在并行流中使用。根据上面介绍,并行流是将Stream切分为多个分片,然后分别对分片进行计算处理得到分片各自的结果,最后这些分片的结果需要合并为同一份总的结果,这个如何合并,就是此处我们需要实现的:

@Override
public BinaryOperator<AtomicInteger> combiner() {
// 多个分段结果处理的策略,直接相加
return (sum1, sum2) -> {
sum1.addAndGet(sum2.get());
return sum1;
};
}

因为我们这里是要做一个数字平方的总和,所以这里对于分片后的结果,我们直接累加到一起即可。

  • finisher方法

我们的收集器目标结果是输出一个累加的Integer结果值,但是为了保证并发流中的线程安全,我们使用AtomicInteger作为了结果容器。也就是最终我们需要将内部的AtomicInteger对象转换为Integer对象,所以finisher方法我们的实现逻辑如下:

@Override
public Function<AtomicInteger, Integer> finisher() {
// 结果处理完成之后对结果的二次处理
// 为了支持多线程并发处理,此处内部使用了AtomicInteger作为了结果累加器
// 但是收集器最终需要返回Integer类型值,此处进行对结果的转换
return AtomicInteger::get;
}
  • characteristics方法

这里呢,我们声明下该Collector收集器的一些特性就行了:

  1. 因为我们实现的收集器是允许并行流中使用的,所以我们声明了CONCURRENT属性;
  2. 作为一个数字累加算总和的操作,对元素的先后计算顺序并没有关系,所以我们也同时声明UNORDERED属性;
  3. 因为我们的finisher方法里面是做了个结果处理转换操作的,并非是一个恒等处理操作,所以这里就不能声明IDENTITY_FINISH属性。

基于此分析,此方法的实现如下:

@Override
public Set<Characteristics> characteristics() {
Set<Characteristics> characteristics = new HashSet<>();
// 指定该收集器支持并发处理(前面也发现我们采用了线程安全的AtomicInteger方式)
characteristics.add(Characteristics.CONCURRENT);
// 声明元素数据处理的先后顺序不影响最终收集的结果
characteristics.add(Characteristics.UNORDERED);
// 注意:这里没有添加下面这句,因为finisher方法对结果进行了处理,非恒等转换
// characteristics.add(Characteristics.IDENTITY_FINISH);
return characteristics;
}

这样呢,我们的自定义收集器就实现好了,如果需要完整代码,可以到文末的github仓库地址上获取。

我们使用下自己定义的收集器看看:

public void testMyCollector() {
Integer result = Stream.of(new Score(1), new Score(2), new Score(3), new Score(4))
.collect(new MyCollector<>(Score::getScore));
System.out.println(result);
}

输出结果:

30

完全符合我们的预期,自定义收集器就实现好了。回头再看下,是不是挺简单的?

总结

好啦,关于Java中Stream的collect用法与Collector收集器的内容,这里就给大家分享到这里咯。看到这里,不知道你是否掌握了呢?是否还有什么疑问或者更好的见解呢?欢迎多多留言切磋交流。

此外:

我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

讲透JAVA Stream的collect用法与原理,远比你想象的更强大的更多相关文章

  1. Java stream的常见用法

    不讲原理,只说用法. 1,集合遍历 public class StreamTest { public static void main(String[] args) { //1 遍历 List< ...

  2. java8 stream().map().collect()用法

    有一个集合: List<User> users = getList(); //从数据库查询的用户集合 现在想获取User的身份证号码:在后续的逻辑处理中要用: 常用的方法我们大家都知道,用 ...

  3. 一文讲透Java序列化

    本文目录 一.序列化是什么 二.为什么需要序列化 三.序列化怎么用 四.序列化深度探秘 4.1 为什么必须实现Serializable接口 4.2 被序列化对象的字段是引用时该怎么办 4.3 同一个对 ...

  4. [java]Stream API——collect、reduce、orElse(x)

    一.collect 1.R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) supplier:一个能创造 ...

  5. Java final 关键字的用法以及原理(7)

    /* final : 最终.作为一个修饰符, 1:可以修饰类,函数,变量. 2:被final修饰的类不可以被继承.为了避免被继承,被子类复写功能. 3:被final修饰的方法不可以被复写. 4:被fi ...

  6. java stream中Collectors的用法

    目录 简介 Collectors.toList() Collectors.toSet() Collectors.toCollection() Collectors.toMap() Collectors ...

  7. Java中stream的详细用法

    来自于:Java 8 stream的详细用法_旅行者-CSDN博客_java stream 一.概述 Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行 ...

  8. java stream collector

    Java Stream API进阶篇 本文github地址 上一节介绍了部分Stream常见接口方法,理解起来并不困难,但Stream的用法不止于此,本节我们将仍然以Stream为例,介绍流的规约操作 ...

  9. Java Stream函数式编程图文详解(二):管道数据处理

    一.Java Stream管道数据处理操作 在本号之前发布的文章<Java Stream函数式编程?用过都说好,案例图文详解送给你>中,笔者对Java Stream的介绍以及简单的使用方法 ...

随机推荐

  1. WIN10 使用注册表设置单应用KIOSK模式(不限win10版本)

    注意事项 下载安装Autologon.exe. 以下示例采用账号:- 账户:'KIOSK'- 密码:'KIOSK' 设置步骤 新建用户 1.进入windows设置->账户->其他用户,点击 ...

  2. 最佳案例 | QQ 相册云原生容器化之路

    关于我们 更多关于云原生的案例和知识,可关注同名[腾讯云原生]公众号~ 福利: ①公众号后台回复[手册],可获得<腾讯云原生路线图手册>&<腾讯云原生最佳实践>~ ②公 ...

  3. web框架的本质、MVC框架MTV框架的介绍

    1.web框架的本质 所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端,基于请求做出响应,客户都先请求,服务端做出对应的响应,按照http协议的请求协议发送请 ...

  4. GIL与普通互斥锁区别,死锁现象,信号量,event事件,进程池与线程池,协程

    GIL与普通互斥锁区别 GIL锁和互斥锁的异同点 相同: 都是为了解决解释器中多个线程资源竞争的问题 异: 1.互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题(线程数据共 ...

  5. Kafka 基础概念及架构

    一.Kafka 介绍 Kafka是⼀个分布式.分区的.多副本的.多⽣产者.多订阅者,基于zookeeper协调的分布式⽇志系统(也可以当做MQ系统),常⻅可以⽤于web/nginx⽇志.访问⽇志,消息 ...

  6. 其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4)

    前言 目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster.神策等框架中都有 Transform 的影子.Transform 听起来很高大上,其本质就是一 ...

  7. 我使用Spring AOP实现了用户操作日志功能

    我使用Spring AOP实现了用户操作日志功能 今天答辩完了,复盘了一下系统,发现还是有一些东西值得拿出来和大家分享一下. 需求分析 系统需要对用户的操作进行记录,方便未来溯源 首先想到的就是在每个 ...

  8. Fail2ban 配置详解 过滤器配置

    Fail2ban自带了很多相关服务日志的过滤器. ### # 包含配置 ### [INCLUDES] before = common.conf # 还包含其他文件中的配置,在加载本配置文件中配置之前先 ...

  9. 第06组Alpha冲刺(3/6)

    目录 1.1 基本情况 1.2 冲刺概况汇报 1.郝雷明 2.鲍凌函 3.曾丽莉 4. 曹兰英 5. 方梓涵 6.董翔云 7.杜筱 8.黄少丹 9. 詹鑫冰 10.吴沅静 1.3 冲刺成果展示 1.1 ...

  10. Python 生成图片验证码

    验证码图片生成 #!/usr/bin/env python # -*- coding: utf-8 -*- # refer to `https://bitbucket.org/akorn/wheezy ...