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

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


  • 首页

  • 归档

  • 搜索

从零开始独立游戏开发学习笔记(十七)--Unity学习笔记(

发表于 2021-11-28

好了,忙的时候结束了。

继续讲述和对象相关的知识。这一章讲使用模式匹配进行类型转换。

  1. 如何安全地格式转换(模式匹配)

由于对象具有多态性。一个具有基类类型的变量是可以存放 derived 类型的变量的值的,但这有可能产生 InvallidCastException。C# 提供了使用模式匹配(pattern match)的格式转换(cast),仅当成功的时候会转换。C# 也提供了 is 和 as 关键字来判断一个值是否为某个类型。

1.1 is 运算符

比如说以下代码:

1
2
3
4
5
6
7
8
9
10
C#复制代码static void FeedMammal(Animal a) {
if (a is Mammmal m)
{
m.Eat();
}
else
{
Console.WriteLine($"{a.GetType().Name} is not a Mammal");
}
}

重点在于:

  1. is 后面并不只是一个类型,而是声明了一个 Mammal 类型的变量。并不是说只能这么写。单单写 a is Mammal 也行,只是这种语法把类型判断和初始化写在一起,也是可行的一种语法。当判断成功的时候,a 的值会被赋予给了 m。
  2. m 的作用域仅仅在于 if 里,甚至连 else 里都无法访问。

1.2 as 运算符

请看以下代码:

1
2
3
4
5
6
7
8
9
10
11
C#复制代码static void TestForMammals(Object o) {
var m = o as Mammal;
if (m != null)
{
Console.WriteLine(m.ToString());
}
else
{
Console.WriteLine($"{o.GetType().Name} is not a Mammal");
}
}
  1. as 运算符执行一次转换。如果成功则转换成对应类型,不成功则返回 null。
  2. 顺便一提,上面的 m != null 也可以换成 m is not null。

1.3 switch 做类型匹配

如下所示的语法也是可以的:

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
C#复制代码static void PatternMatchingSwitch(System.ValueType val)
{
switch (val)
{
case int number:
Console.WriteLine(number);
break;
case long number:
Console.WriteLine(number);
break;
case decimal number:
Console.WriteLine(number);
break;
case float number:
Console.WriteLine(number);
break;
case double number:
Console.WriteLine(number);
break;
case null:
Console.WriteLine("val is a nullable type with the null value");
break;
default:
Console.WriteLine("Could not convert " + val.ToString());
break;
}
}
  1. 模式匹配的场景

现代开发经常要用到来自各种不同地方的数据源,因此数据类型也都不一致。

于是文章采用了这么一个场景–在一个收费站收费。根据高峰期和车型收费。
难点在于,数据来源可能是多个不同的外部系统。那么首先假设有这么三个系统(3 个 namespace):

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
C#复制代码namespace ConsumerVehicleRegistration
{
public class Car
{
public int Passengers { get; set; }
}
}

namespace CommercialRegistration
{
public class DeliveryTruck
{
public int GrossWeightClass { get; set; }
}
}

namespace LiveryRegistration
{
public class Taxi
{
public int Fares { get; set; }
}

public class Bus
{
public int Capacity { get; set; }
public int Riders { get; set; }
}
}

即,数据可能以不同的 class 形式存在。

2.1 最基础的收费

写一个最基础的收费类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C#复制代码using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;

namespace toll_calculator
{
public class TollCalculator
{
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
}

这里使用了一个 switch expression 的语法(非 switch statement)。语法一看大概也知道是怎么回事。因为整个是一个 switch,因此 => 跟的就是 return 的值。

  1. { } 则是匹配所有的 非 null 的 object。必须写在后面,否则就被第一个返回了。
  2. null 则是匹配 null。

2.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
C#复制代码public class TollCalculator
{
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,

Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,

Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,

DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,

{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
  1. when 的用法也是简洁明了。当并等于某一个值,而是一个判断语句的时候用 when。
  2. 以上的代码有部分比较重复。比如对于 car 和 taxi,每个乘客数量都要写一整行代码。可以被简化为以下代码:
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
C#复制代码public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => c.Passengers switch
{
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},

Taxi t => t.Fares switch
{
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},

Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus b => 5.00m,

DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck t => 10.00m,

{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
  1. 可以看到根本没有新的语法。而是再写一个 switch expression。
  2. _ 表示匹配其他所有情况。同理也不能写在前面,因为一定会被匹配上。

2.3 根据高峰时间收费

假设有这么一个需求。周末正常收费。工作日的话,早上的入流量和晚上的出流量双倍收费。其他时间 1.5 倍收费。凌晨则减少为 0.75。

如果写成 if 语句,写倒是可以写,但是效果如下:

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
C#复制代码public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
{
if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
(timeOfToll.DayOfWeek == DayOfWeek.Sunday))
{
return 1.0m;
}
else
{
int hour = timeOfToll.Hour;
if (hour < 6)
{
return 0.75m;
}
else if (hour < 10)
{
if (inbound)
{
return 2.0m;
}
else
{
return 1.0m;
}
}
else if (hour < 16)
{
return 1.5m;
}
else if (hour < 20)
{
if (inbound)
{
return 1.0m;
}
else
{
return 2.0m;
}
}
else // Overnight
{
return 0.75m;
}
}
}

可以用,但非常难读,也不好改。

2.3.1 使用模式匹配以及其他技巧来简化代码

仅仅使用模式匹配来匹配所有可能性也不好,依然复杂,因为我们有很多种组合情况。

2.3.1.1 周末还是工作日

第一个条件是是否为周末。那么专门为此写一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
C#复制代码// 注意 timeOfToll.DayOfWeek 和 DayOfWeek.Monday 中的 DayOfWeek 不是一个东西。
// 前者是 DateTime 类型的一个属性,后者是一个 enum 类型。
// 前者的值也为 DayOfWeek 类型
public static bool IsWeekday(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch {
DayOfWeek.Monday => true,
DayOfWeek.Tuesday => true,
DayOfWeek.Wednesday => true,
DayOfWeek.Thursday => true,
DayOfWeek.Friday => true,
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false
}

还可以再简化:

1
2
3
4
5
6
C#复制代码public static bool IsWeekday(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch {
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
}

2.3.1.2 一天的时间段

先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C#复制代码public enum TimeBand
{
MorningRush,
Daytime,
EvenignRush,
Overnight
}

public static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
{
> 19 or < 6 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
> 16 => TimeBand.EvenignRush,
_ => TimeBand.Daytime
};
  1. 使用了 enum 来将一天的多个时间段分配值。
  2. 使用了 > 19 or < 6 这种语法,> 和 < 以及 or 都是在 C# 9.0 后引入的。当然还有 >=,<=,and,not 这些语法。(什么你问为什么没有 = 的语法,因为不需要,直接写 6 就是 =6 了)

2.3.1.3 最终代码

有了以上两个函数后,代码就可以简化为这种 tuple pattern 形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C#复制代码public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
(IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, true) => 1.50m,
(true, TimeBand.Daytime, false) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, true) => 0.75m,
(true, TimeBand.Overnight, false) => 0.75m,
(false, TimeBand.MorningRush, true) => 1.00m,
(false, TimeBand.MorningRush, false) => 1.00m,
(false, TimeBand.Daytime, true) => 1.00m,
(false, TimeBand.Daytime, false) => 1.00m,
(false, TimeBand.EveningRush, true) => 1.00m,
(false, TimeBand.EveningRush, false) => 1.00m,
(false, TimeBand.Overnight, true) => 1.00m,
(false, TimeBand.Overnight, false) => 1.00m,
};

当然,很多条件可以简化:

1
2
3
4
5
6
7
8
9
10
11
C#复制代码public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
(IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
(false, _, _) => 1.00m,
};

然后可以把 3 个返回 1.00m 的用 _ 代替:

1
2
3
4
5
6
7
8
9
C#复制代码public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
(IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
_ => 1.00m,
};

本文转载自: 掘金

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

架构图设计:一篇教你如何画好项目中各种架构图的详细教程 架构

发表于 2021-11-28

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

架构图基本概念

  • 架构是由系统组件,以及组件间的相互关系构成的集合体
  • 架构图则是用来表达这种相互关系的集合体的载体,作用有两个:
    • 划分目标系统边界
    • 将目标系统的结构可视化

架构的分类

业务架构

  • 业务架构: 使用一套方法论或者逻辑对项目所涉及到的业务进行边界划分,业务架构的关键是熟悉业务
  • 比如一个团购网站,需要将商品类目,商品,订单,订单服务,支付,退款等进行清晰划分
  • 业务架构不需要考虑使用什么技术开发,并发量以及相关项目实施问题
    在这里插入图片描述

应用架构

  • 应用架构: 对整个系统实现的总体架构,需要指出系统的层次,系统开发的原则,系统各个层次的应用服务
  • 比如将系统分为数据层,服务层,通信层,展现层,并且细分每个层次的应用服务
    在这里插入图片描述

数据架构

  • 数据架构: 是一套对于存储数据的架构逻辑.根据各个系统应用场景,不同时间段的应用场景,对数据进行诸如数据异构,读写分离,缓存使用,分布式数据策略等划分
  • 数据架构主要解决三个问题:
    • 系统需要什么样的数据
    • 如何存储这些数据
    • 如何进行数据架构设计
  • 比如大数据架构项目的架构逻辑:
    在这里插入图片描述

技术架构

  • 技术架构:
    • 应用架构只关心需要哪些应用系统,哪些平台来满足业务目标的需求,而不会关心在整个构建过程中需要使用哪些技术
    • 技术架构则是根据应用架构的需求,识别需要的技术需求,进行技术选型,将各个关键技术和技术之间的关系描述清楚
  • 技术架构主要解决四个问题:
    • 纯技术层面的分层
    • 开发框架的选择
    • 开发语言的选择
    • 涉及非功能性需求的技术选择

架构图设计

架构图设计步骤

  • 了解需要设计的架构图的类型
  • 确认架构图中的关键要素:
    • 产品
    • 技术
    • 服务
  • 梳理关键要素之间的联系:
    • 包含
    • 支撑
    • 同级并列
  • 输出关联关系清晰的架构图

架构设计内容

  • 架构设计需要表达的内容:
    • 系统的边界以及与外界的关系
    • 系统的内部组成以及各组成部分之间的关系
    • 关键流程
    • 数据的流转
    • 开发技术的选型
    • 如何部署以及需要哪些资源

架构设计方式

  • 系统边界以及与外界的关系: 逻辑架构图, 总体架构, 顶层设计
    • 架构图以方框的形式将各个主要实体画出来,通过线条来表现相互之间的关系
    • 每一个实体的边界,每一部分的作用都需要在图中明确
  • 系统的内部组成以及各组成部分之间的关系: 逻辑架构图
    • 架构图以方框的形式将系统内组成的各个实体画出来,通过线条来表现相互之间的关系
    • 系统内的组成可以是逻辑上的模块,也可以是物理上实实在在的微服务
  • 关键流程: 流程图, 时序图
    • 这一类图关键在于逻辑的一致性
    • 流程中的实体要和逻辑架构中的实体一一对应
  • 数据的流转: 数据架构图
    • 类似流程图,只是数据架构图强调的是数据流转关系,所以箭头有特定含义
  • 开发技术的选型: 开发架构图
    • 架构图以方框形式表示
    • 一般采用分层的画法. 比如框架层, 数据层, 前端层
    • 在每一层标识采用的技术框架
  • 如何部署以及需要哪些资源: 物理架构图
    • 对应逻辑架构图
    • 明确需要部署的节点,需要用到的中间件与数据库以及需要开同的防火墙

本文转载自: 掘金

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

【死磕Java并发】-----JUC之Condition

发表于 2021-11-28

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

–

在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活。下图是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):

Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. await(long time, TimeUnit unit):造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  3. awaitNanos(long nanosTimeout):造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  4. awaitUninterruptibly():造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline):造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  7. signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

Condtion的实现

获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:

1
2
kotlin复制代码public class ConditionObject implements Condition, java.io.Serializable {
}

等待队列

每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。我们看Condition的定义就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;

//头节点
private transient Node firstWaiter;
//尾节点
private transient Node lastWaiter;

public ConditionObject() {
}

/** 省略方法 **/
}

从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:

Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。

等待

调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。

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
scss复制代码public final void await() throws InterruptedException {
// 当前线程中断
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入等待队列
Node node = addConditionWaiter();
//释放锁
long savedState = fullyRelease(node);
int interruptMode = 0;
/**
* 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
* 直到检测到此节点在同步队列上
*/
while (!isOnSyncQueue(node)) {
//线程挂起
LockSupport.park(this);
//如果已经中断了,则退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//竞争同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理下条件队列中的不是在等待条件的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

此段代码的逻辑是:首先将当前线程新建一个节点同时加入到条件队列中,然后释放当前线程持有的同步状态。然后则是不断检测该节点代表的线程释放出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。

加入条件队列(addConditionWaiter())源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码private Node addConditionWaiter() {
Node t = lastWaiter; //尾节点
//Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点
if (t != null && t.waitStatus != Node.CONDITION) {
//清除条件队列中所有状态不为Condition的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//当前线程新建节点,状态CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
/**
* 将该节点加入到条件队列中最后一个位置
*/
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}

该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清楚所有状态不为Condition的节点。

fullyRelease(Node node),负责释放该线程持有的锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码final long fullyRelease(Node node) {
boolean failed = true;
try {
//节点状态--其实就是持有锁的数量
long savedState = getState();
//释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}

isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true

1
2
3
4
5
6
7
8
9
10
kotlin复制代码final boolean isOnSyncQueue(Node node) {
//状态为Condition,获取前驱节点为null,返回false
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//后继节点不为null,肯定在CLH同步队列中
if (node.next != null)
return true;

return findNodeFromTail(node);
}

unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}

通知

调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。

1
2
3
4
5
6
7
8
9
java复制代码public final void signal() {
//检测当前线程是否为拥有锁的独
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//头节点,唤醒条件队列中的第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first); //唤醒
}

该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。

doSignal(Node first):唤醒头节点

1
2
3
4
5
6
7
8
9
ini复制代码private void doSignal(Node first) {
do {
//修改头结点,完成旧头结点的移出工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}

doSignal(Node first)主要是做两件事:1.修改头节点,2.调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。transferForSignal(Node first)源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码final boolean transferForSignal(Node node) {
//将该节点从状态CONDITION改变为初始状态0,
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;

//将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点
Node p = enq(node);
int ws = p.waitStatus;
//如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;

整个通知的流程如下:

  1. 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
  2. 如果线程已经获取了锁,则将唤醒条件队列的首节点
  3. 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中
  4. 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。

总结

一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。

Condition的应用

只知道原理,如果不知道使用那就坑爹了,下面是用Condition实现的生产者消费者问题:

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
csharp复制代码public class ConditionTest {
private LinkedList<String> buffer; //容器
private int maxSize ; //容器最大
private Lock lock;
private Condition fullCondition;
private Condition notFullCondition;

ConditionTest(int maxSize){
this.maxSize = maxSize;
buffer = new LinkedList<String>();
lock = new ReentrantLock();
fullCondition = lock.newCondition();
notFullCondition = lock.newCondition();
}

public void set(String string) throws InterruptedException {
lock.lock(); //获取锁
try {
while (maxSize == buffer.size()){
notFullCondition.await(); //满了,添加的线程进入等待状态
}

buffer.add(string);
fullCondition.signal();
} finally {
lock.unlock(); //记得释放锁
}
}

public String get() throws InterruptedException {
String string;
lock.lock();
try {
while (buffer.size() == 0){
fullCondition.await();
}
string = buffer.poll();
notFullCondition.signal();
} finally {
lock.unlock();
}
return string;
}
}

Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。

本文转载自: 掘金

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

Exchange漏洞分析:SSRF RCE

发表于 2021-11-28

0x00 前言

在今年3月份,微软公布了多个Microsoft Exchange的高危漏洞。ProxyLogon是Exchange历史上最具影响力的漏洞之一,有上千台Exchange服务器被植入了webshell后门。

0x01 漏洞描述

CVE-2021-26855是一个SSRF漏洞,利用该漏洞可以绕过Exchange的身份验证,CVE-2021-27065是一个文件写入漏洞。二者结合可以在未登录的状态下写入webshell。

0x02 影响范围

Exchange Server 2019 < 15.02.0792.010

Exchange Server 2019 < 15.02.0721.013

Exchange Server 2016 < 15.01.2106.013

Exchange Server 2013 < 15.00.1497.012

0x03 调试环境及工具

版本
操作系统 Windows Server 2012
应用名称 Microsoft Exchange Server 2016
.NET反编译调试器 DnSpy
.NET反编译对比器 Telerik

【一>所有资源获取<一】
1、200份很多已经买不到的绝版电子书
2、30G安全大厂内部的视频资料
3、100份src文档
4、常见安全面试题
5、ctf大赛经典题目解析
6、全套工具包
7、应急响应笔记

0x04 CVE-2021-26855 SSRF漏洞

1. 补丁查看

通过对比补丁内容,快速定位漏洞位置。

Exchange版本号查看:

打开 ExchangeManagement Shell,并执行命令:

Get-ExchangeServer | fl name,edition,admindisplayversion

image.png

版本对应关系可以在微软官网中查看:

image.png

搜索对应版本的补丁(Exchange 2016 RTM的补丁未找到,所以直接用 Exchange CU11 的补丁分析):

image.png

下载补丁KB5000871,使用winrar解压cab文件,解压后是一个msp文件,使用7-zip 对msp格式的文件内容进行提取。

image.png

使用Telerik 对补丁内容做对比,内容有差异的dll文件如下,漏洞存在于Microsoft.Exchange.FrontEndHttpProxy.dll中:

image.png

使用Reflector对dll文件反编译并导出源码,方便对关键字进行搜索。

2. 漏洞调试

使用dnSpy对Microsoft.Exchange.FrontEndHttpProxy.dll 文件进行反编译,定位到class BEResourceRequestHandler,下断点。

image.png

点击 【调试】–>【附加到进程】 MSExchangeECPAppPool,使用ProcessExplorer查看对应的进程ID。

image.png

使用进程ID进行过滤(注:必须以管理员权限运行dnSpy,否则无法获取进程信息)。

image.png

在BackEndCookieEntryParse 的TryParse()函数下断点,bp发请求,触发断点。

image.png

image.png

首先会根据uri来判断请求的协议类型,如果请求协议为ecp,即请求uri为/ecp/。跟踪进入CanHandle()函数,验证cookie及uri的合法性。

image.png

获取cookie中X-BEResource的值

image.png

判断uri 是否合法

image.png

uri以 js,css,html,flt,xap等后缀结尾的文件(没有验证文件是否存在)都是合法的。拼接后 /ecp/xx.(js|css) 等都是有效uri。

接着对X-BEResource的值进行处理

image.png

image.png

获取到X-BEResource的值

image.png

并使用‘~’分割得到数组array

image.png

image.png

其中array[0] 为fqdn;array[1] 为version

接着进行版本比较

image.png

image.png

如果版本的值小于 E15MinVersion,进入if判断语句,并将类成员变量ProxyToDownLevel赋值为True,之后会调用身份认证函数EcpProxyRequestHandler.AddDownLevelProxyHeaders进行身份认证,如果没有通过认证,则返回401错误。同理如果version大于E15MinVersion则跳过if判断从而绕过身份认证。

经过一系列的解析

image.png

image.png

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-582TsK39-1638085393980)(upload-images.jianshu.io/upload_imag…)]

可以看到host的值是win2012,这个值我们可控,最后经过一系列的拼接得到AbsoluteUri的值:https://host/autodiscover/autodiscover.xml ,autodiscover.xml 文件中包含有LegacyDN 的值。

进入CreateServerRequest函数,访问uri,

image.png

进入PrepareServerRequest函数,会进行授权认证判断,

image.png

接着进入AddProtocolSpecificHeadersToServerRequest,

image.png

由于ProxyToDownLevel被置为false不会进入if语句,从而导致可以直接绕过认证,进而在未授权的状态下获取autodiscover.xml文件内容。

0x05 CVE-2021-27065 任意文件写入漏洞

在Exchange服务器上依次打开【管理中心】 à 【服务器】 à 【虚拟目录】 à 【OAB虚拟目录】。

image.png

由于外部URL的内容可控,所以可在URL写入一句话木马(其中URL必须以http开头,以保持外部URL参数的合法性)。

image.png

之后可以通过重置虚拟目录,来把配置内容写入指定的路径。

image.png

注:路径必须是UNC路径格式:\host\ShareFolder\aaa.xml

修改路径为:\win2012.test.local\C$\test.aspx

image.png

可以看到OAB VirtualDirectory的配置信息已经被写入 C:/test.aspx。

image.png

上述操作请求内容如下:

写入配置

1
ini复制代码POST /ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=pM2NWg8xu0euTUqTjiLwzquHekjm6dgIw6lt6YfDyflndCz1iGsGnnhEivzKafJL9vhOxpqYuAU.

请求中有一个关键参数msExchEcpCanary,如果没有这个参数,服务端返回500错误。这个参数的值可以利用CVE-2021-26855 SSRF漏洞通过多次请求获取。

0x06 漏洞利用过程

整个过程都是在未登录的状态下,利用SSRF漏洞访问内部资源,Cookie中的 X-BEResource字段内容为要访问的资源链接。利用过程如图:

image.png

  1. 获取server name,/ecp/xx.js,cookie中 mail.skyeye.com 为目标exchange的域名;

image.png

  1. 通过SSRF漏洞读取autodiscover.xml文件,获取LegacyDN的值;

image.png

  1. 利用Legacy DN获取SID;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dY4aGMjM-1638085394017)(upload-images.jianshu.io/upload_imag…)]

  1. 利用SID获取Session、msExchEcpCanary;

image.png

  1. 写入shell。

image.png

cookie中的 X-BEResource字段内容为利用SSRF漏洞访问的内网资源。

image.png

指定保存路径

image.png

webshell路径为:

https://[ip]/owa/auth/dkYAIm7ELFjJ3KVzXb1Q.aspx

0x07 检测修复方案

检测方案:
针对上述漏洞,微软发布了一款工具,用于帮助用户检测Exchange是否被黑客利用相关漏洞入侵。

脚本 描述
EOMT.ps1 通过 URL 重写配置缓解 CVE-2021-26855
ExchangeMitigations.ps1 该脚本包含4个缓解措施,以帮助解决以下漏洞:cve-2021-26855、cve-2021-26857、cve-2021-27065、cve-2021-26858
http-vuln-cve2021-26855.nse nmap扫描脚本,检测指定URL是否存在CVE-2021-26855漏洞
Test-ProxyLogon.ps1 该脚本检查CVE-2021-26855、26858、26857和27065漏洞是否存在被利用的迹象

修复方案:

安装微软提供的漏洞补丁,相关漏洞补丁如下:

msrc.microsoft.com/update-guid…

msrc.microsoft.com/update-guid…

本文转载自: 掘金

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

SpringBoot自动配置之AutoConfigurati

发表于 2021-11-28

本文基于SpringBoot 2.5.7版本进行讲解

在上一篇文章,SpringBoot自动配置之@SpringBootApplication注解 - SpringBoot自动配置(一)说到,SpringBoot自动配置的核心就在@EnableAutoConfiguration注解上,这个注解通过@Import(AutoConfigurationImportSelector)来完成自动配置。

所以说,SpringBoot自动配置的奥秘就隐藏在AutoConfigurationImportSelector类中。

阅读须知

在分析AutoConfigurationImportSelector如何实现SpringBoot自动配置之前,读者需要了解:

  1. @Import注解是什么?有什么用?
  2. ImportSelector接口是什么?有什么用?

AutoConfigurationImportSelector类

了解一个类,我们首先就要先看这个类的定义:

1
2
java复制代码public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

我们可以看到它实现了DeferredImportSelector接口,那么接下来再来看看DeferredImportSelector接口的定义:

1
java复制代码public interface DeferredImportSelector extends ImportSelector {

可以看到,DeferredImportSelector接口又继承了ImportSelector接口。这样,我们就明白了,AutoConfigurationImportSelector类必定实现了selectImports()方法,这个方法应该就是SpringBoot能够实现自动配置的核心。

selectImports()方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private static final String[] NO_IMPORTS = {};

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断SpringBoot是否开启自动配置
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 获取需要被引入的自动配置信息
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

selectImports()方法的源码没有多少。

isEnabled()方法判断SpringBoot是否开启了自动配置。若开启就通过getAutoConfigurationEntry()来获取需要配置的Bean全限定名数组,否则就直接返回空数组。

isEnabled():判断SpringBoot是否开启自动配置

1
2
3
4
5
6
7
java复制代码protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
// 若调用该方法的类是AutoConfigurationImportSelector,那么就获取EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY的值,默认为true
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}

EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY是什么?我们看下它的定义:

1
java复制代码String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

看到这里,我们可以猜到这就是在配置文件application.yml或者application.properties中的配置。因此,我们可以在配置文件中来决定SpringBoot是否开启自动配置。

当我们没有配置的时候,默认就是开启自动配置的。

getAutoConfigurationEntry()方法:获取需要自动配置的bean信息

话不多说,先上源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 判断是否开启自动配置
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取@EnableAutoConfiguration注解的属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 从spring.factories文件中获取配置类的全限定名数组
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重
configurations = removeDuplicates(configurations);
// 获取注解中exclude或excludeName排除的类集合
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查被排除类是否可以实例化,是否被自动配置所使用,否则抛出异常
checkExcludedClasses(configurations, exclusions);
// 去除被排除的类
configurations.removeAll(exclusions);
// 使用spring.factories配置文件中配置的过滤器对自动配置类进行过滤
configurations = getConfigurationClassFilter().filter(configurations);
// 抛出事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

在这里,我们只需要知道这个getAutoConfigurationEntry()方法是用来获取需要自动配置的bean信息,以及里面每个方法做了什么,有个大概的印象就可以了。

下面会对每个方法作更近一步的讲解。

getAttributes():获取@EnableAutoConfiguration注解属性

1
2
3
4
5
6
7
java复制代码protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
String name = getAnnotationClass().getName();
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
Assert.notNull(attributes, () -> "No auto-configuration attributes found. Is " + metadata.getClassName()
+ " annotated with " + ClassUtils.getShortName(name) + "?");
return attributes;
}

image.png

看了上面断点的图,我们大概就明白了,这个方法就是获取@EnableAutoConfiguration注解的属性。

getCandidateConfigurations():从spring.factories文件获取需要配置的bean

1
2
3
4
5
6
7
java复制代码protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

getCandidateConfigurations()方法通过SpringFactoriesLoader的loadFactoryNames()方法从所有的spring.factories文件中获取需要配置的bean全限定名列表。

这里,对于SpringFactoriesLoader类的loadFactoryNames()方法就不展开讲了。有兴趣的可以自己阅读下源码。

到了这里,可能有很多人都疑惑了。spring.factories文件是什么?它又在哪里?

spring.factories文件本质上与properties文件相似,其中包含一组或多组键值对。其中,key的取值是接口的全限定名,value的取值是接口实现类的全限定名。一个接口可以设置多个实现类,不同实现类之间使用,隔开。例如:

1
2
3
ini复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

SpringFactoriesLoader会扫描所有jar包类路径下的META-INF/spring.factories文件,并获取指定接口的配置。

这里提一句,getCandidateConfigurations()方法获取的是EnableAutoConfiguration接口的配置。

removeDuplicates():去重

1
2
3
java复制代码protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}

我们知道SpringFactoriesLoader的loadFactoryNames()方法会从所有jar包类路径下的META-INF/spring.factories读取配置。就是说会从不同的spring.factories文件中读取配置,那么就有可能会出现配置了相同的类,这里就是对这些数据进行去重。

getExclusions():获取注解的exclude和excludeName属性配置的需要排除的类全限定名集合

1
2
3
4
5
6
7
java复制代码protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}

getExclusions方法就是将上面getAttributes()方法获取到@EnableAutoConfiguration注解的exclude和excludeName属性的值加入到excluded集合中。

checkExcludedClasses():检查排除类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
List<String> invalidExcludes = new ArrayList<>(exclusions.size());
for (String exclusion : exclusions) {
// 判断该类是否可以实例化并且自动配置类列表是否包含该类
if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
invalidExcludes.add(exclusion);
}
}
// 无效排除列表若不为空,抛出异常
if (!invalidExcludes.isEmpty()) {
handleInvalidExcludes(invalidExcludes);
}
}

检查被排除的类是否可以实例化以及自动配置类列表是否包含该类。如果存在无效排除类,那么就抛出异常。

getConfigurationClassFilter():获取配置类过滤器

1
2
3
4
5
6
7
8
9
10
11
java复制代码private ConfigurationClassFilter getConfigurationClassFilter() {
if (this.configurationClassFilter == null) {
// 获取AutoConfigurationImportFilter过滤器列表
List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
for (AutoConfigurationImportFilter filter : filters) {
invokeAwareMethods(filter);
}
this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
}
return this.configurationClassFilter;
}

getConfigurationClassFilter()方法通过getAutoConfigurationImportFilters()方法获取到spring.factories文件中AutoConfigurationImportFilter接口的配置,然后将其封装到ConfigurationClassFilter对象中。

总结

上面,我们介绍了AutoConfigurationImportSelector类是ImportSelector的实现类,实现了selectImports()方法。selectImports()方法又调用getAutoConfigurationEntry()方法从spring.factories文件中读取配置类的全限定名列表,并进行过滤,最终得到需要自动配置的类全限定名列表。

本文转载自: 掘金

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

AbstractEventExecutorGroup 1 类

发表于 2021-11-28

1 类继承关系

image.png

2 代码详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
java复制代码/*
* Copyright 2013 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.util.concurrent;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static io.netty.util.concurrent.AbstractEventExecutor.*;


/**
* Abstract base class for {@link EventExecutorGroup} implementations.
*/
public abstract class AbstractEventExecutorGroup implements EventExecutorGroup {
@Override
public Future<?> submit(Runnable task) {
return next().submit(task);
}

@Override
public <T> Future<T> submit(Runnable task, T result) {
return next().submit(task, result);
}

@Override
public <T> Future<T> submit(Callable<T> task) {
return next().submit(task);
}

@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
return next().schedule(command, delay, unit);
}

@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
return next().schedule(callable, delay, unit);
}

@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
return next().scheduleAtFixedRate(command, initialDelay, period, unit);
}

@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
return next().scheduleWithFixedDelay(command, initialDelay, delay, unit);
}

@Override
public Future<?> shutdownGracefully() {
return shutdownGracefully(DEFAULT_SHUTDOWN_QUIET_PERIOD, DEFAULT_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
}

/**
* @deprecated {@link #shutdownGracefully(long, long, TimeUnit)} or {@link #shutdownGracefully()} instead.
*/
@Override
@Deprecated
public abstract void shutdown();

/**
* @deprecated {@link #shutdownGracefully(long, long, TimeUnit)} or {@link #shutdownGracefully()} instead.
*/
@Override
@Deprecated
public List<Runnable> shutdownNow() {
shutdown();
return Collections.emptyList();
}

@Override
public <T> List<java.util.concurrent.Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
return next().invokeAll(tasks);
}

@Override
public <T> List<java.util.concurrent.Future<T>> invokeAll(
Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
return next().invokeAll(tasks, timeout, unit);
}

@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
return next().invokeAny(tasks);
}

@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return next().invokeAny(tasks, timeout, unit);
}

@Override
public void execute(Runnable command) {
next().execute(command);
}
}

3 总结

如上看到,AbstractEventExecutorGroup基本上什么都没做,都是将操作委托给next(), 然后这个方法的实现还不在这个类中。

本文转载自: 掘金

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

Mysql哪些字段适合建立索引 1 数据库建立索引常用的规则

发表于 2021-11-28

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

1 数据库建立索引常用的规则如下:

1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:

  • A、正确选择复合索引中的主列字段,一般是选择性较好的字段;
  • B 、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
  • C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
  • D、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;

8、频繁进行数据操作的表,不要建立太多的索引;
9、删除无用的索引,避免对执行计划造成负面影响;

以上是一些普遍的建立索引时的判断依据。
索引的建立必须慎重,对每个索引的必要性都应该经过仔细分析,要有建立的依据。
因为太多的索引与不充分、不正确的索引对性能都毫无益处:在表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。 另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大。
总的来说,小型表肯定不建索引,
或者数据库记录在亿条数据级以上,还是建议使用非关系型数据库。
还有些特殊字段的数据库,比如BLOB,CLOB字段肯定也不适合建索引。
其实这个问题更感觉偏向于做软件项目的一种经验。

2、对千万级MySQL数据库建立索引的事项及提高性能的手段

一、注意事项:

首先,应当考虑表空间和磁盘空间是否足够。我们知道索引也是一种数据,在建立索引的时候势必也会占用大量表空间。因此在对一大表建立索引的时候首先应当考虑的是空间容量问题。
其次,在对建立索引的时候要对表进行加锁,因此应当注意操作在业务空闲的时候进行。

二、性能调整方面:

首当其冲的考虑因素便是磁盘I/O。物理上,应当尽量把索引与数据分散到不同的磁盘上(不考虑阵列的情况)。逻辑上,数据表空间与索引表空间分开。这是在建索引时应当遵守的基本准则。

其次,我们知道,在建立索引的时候要对表进行全表的扫描工作,因此,应当考虑调大初始化参数db_file_multiblock_read_count的值。一般设置为32或更大。

再次,建立索引除了要进行全表扫描外同时还要对数据进行大量的排序操作,因此,应当调整排序区的大小。

9i之前,可以在session级别上加大sort_area_size的大小,比如设置为100m或者更大。

9i以后,如果初始化参数workarea_size_policy的值为TRUE,则排序区从pga_aggregate_target里自动分配获得。

最后,建立索引的时候,可以加上nologging选项。以减少在建立索引过程中产生的大量redo,从而提高执行的速度。

MySql在建立索引优化时需要注意的问题

设计好MySql的索引可以让你的数据库飞起来,大大的提高数据库效率。设计MySql索引的时候有一下几点注意:

1,创建索引

对于查询占主要的应用来说,索引显得尤为重要。很多时候性能问题很简单的就是因为我们忘了添加索引而造成的,或者说没有添加更为有效的索引导致。如果不加索引的话,那么查找任何哪怕只是一条特定的数据都会进行一次全表扫描,如果一张表的数据量很大而符合条件的结果又很少,那么不加索引会引起致命的性能下降。但是也不是什么情况都非得建索引不可,比如性别可能就只有两个值,建索引不仅没什么优势,还会影响到更新速度,这被称为过度索引。

2,复合索引

比如有一条语句是这样的:

1
sql复制代码select * from users where area=’beijing’ and age=22;

如果我们是在area和age上分别创建单个索引的话,由于mysql查询每次只能使用一个索引,所以虽然这样已经相对不做索引时全表扫描提高了很多效
率,但是如果在area、age两列上创建复合索引的话将带来更高的效率。如果我们创建了(area, age,
salary)的复合索引,那么其实相当于创建了(area,age,salary)、(area,age)、(area)三个索引,这被称为最佳左前缀
特性。因此我们在创建复合索引时应该将最常用作限制条件的列放在最左边,依次递减。

3,索引不会包含有NULL值的列

只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

4,使用短索引

对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的 列,如果在前10 个或20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

5,排序的索引问题

mysql查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。

6,like语句操作

一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%a%” 不会使用索引而like “aaa%”可以使用索引。

7,不要在列上进行运算

1
sql复制代码select * from users where YEAR(adddate)

8,不使用NOT IN和操作

NOT IN和操作都不会使用索引将进行全表扫描。NOT IN可以NOT EXISTS代替,id3则可使用id>3 or id

添加索引示例:

1
2
3
4
5
6
7
sql复制代码CREATE INDEX IDX_AUDITSTATUS ON [shanghaiDB].[dbo].[Activity](AUDITSTATUS) WITH(ONLINE=ON)

CREATE INDEX IDX_ANUMMID ON [nantongDB].[dbo].[Orders](ANUM,MID) WITH(ONLINE=ON)

CREATE INDEX IDX_SiteCode ON Usercenter.[dbo].MO(SiteCode) WITH(ONLINE=ON)

CREATE INDEX IDX_AccessDt ON [all].[dbo].[AccessLog](AccessDt) WITH(ONLINE=ON)

Create index注意n如果是大表建立索引,切记加上ONLINE参数

这几天在做数据库的优化,有个2亿记录的表,发现需要添加一个联合索引,结果就采用普通的create index index_name on tablename (entp_id,sell_date),结果悲剧了,把所有的DML语句都阻塞了,导致系统不能正常使用,还好是晚上10点,用户不是非常多,1个小时候,索引结束,阻塞解决;

上网查了一下,如果加上 online参数后,就可以在线做索引,而不需要阻塞所有的DML语句,血的教训,拿出来与各位共勉,具体online与不加online区别如下:

  1. DML操作对create index 的影响。 如果在create的时候,有其他的进程在对这个index 所对应的数据进行DML操作,create会受影响:
1
2
3
sql复制代码SQL> create table test (id number, name varchar2(20));

Table created.

然后重新开一个session:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SQL> insert into test values (1,'lms');

1 row created.

<no commit>

SQL> create index t1 on test(id);
create index t1 on test(id)
*
ERROR at line 1:
ORA-00054: resource busy and acquire with NOWAIT specified
  1. 加online这个参数,这个参数加上以后,除了create过程中index 保持online状态,Oracle还会在create index之前等待所有DML操作结束,然后得到DDL锁,开始create.
1
2
3
4
5
6
7
8
9
sql复制代码SQL> create index t1 on test(id) online;
<hold before commit>
<after commit>

SQL> commit;

Commit complete.

Index altered.

如果不commit,上面的操作就会一直hold。

所以以后create索引和rebuild索引的时候最好加上online。

本文转载自: 掘金

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

文件上传下载和三层架构 三层架构

发表于 2021-11-28

一 、文件上传

  • method 需要使用 post 提交,get 限制了数据大小。
  • enctype 需使用 multipart/form-data,不然直接报错(需要二进制数据)。
  • 需要提供 fifile 控件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<span style="color: red">${errorMsg}</span><br/>
<form action="/fileType" method="post" enctype="multipart/form-data">
<p>账号:<input type="text" name="username"/></p>
<%--input 便签 type选择file 即选择了一个上传文件控件--%>
<p>头像:<input type="file" name="Default"/></p>
<input type="submit" value="注册">
</form>
</body>
</html>

注意: enctype=”multipart/form-data” 提交的数据,getParameter() 无法获取到。

二、Servlet3.0 文件上传

1. API

HttpServletRequest 提供了两个方法用于从请求中解析上传的文件

返回值 方法 作用
Part getPart(String name) 用于获取请求中指定 name 的文件
Collection getParts() 获取请求中全部的文件

Part 中常用方法:

返回值 方法 作用
void write(String fifileName) 直接把接收到的文件保存到磁盘中
void getContentType() 获取文件的类型 MIME
String getHeader(String name) 获取请求头信息
long getSize() 获取文件的大小

​ 要给 Servlet 贴一个标签 @MultipartConfig 然后使用 getPart() 获取请求中指定 name 的文件到 Part 对象中,再使用 write 方法把文件保存到指定目录就可以了

2、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@WebServlet("/fileUpload")
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 普通控件数据还是使用 getParameter 方法来获取
System.out.println("username:" + req.getParameter("username"));
// 文件控件数据获取
Part part = req.getPart("headImg");
// 保存到磁盘上
part.write("D:/headImg.png");
}
}

三、文件上传细节

1. 获取上传文件名

​ 以前是拷贝文件(自传自存),知道文件类型;现在是用户传,程序接收,接收到文件时,涉及保存到磁盘使用什么文件名以及文件类型的问题,所有需要先获取到文件名及文件类型。可使用 Part API 获取:

返回值 方法 作用
String getHeader(“contentdisposition”) Tocmat 8.0 之前使用通过请求头获取文件名,需截取字符串
String getSubmittedFileName() Tomcat8.0 之后提供的直接获取文件名方式

2. 文件名相同覆盖现有文件

​ 若上传得文件名相同会导致覆盖服务器之前已上传的的文件,咱们的解决方法就是自己给文件起一个唯一的名称,确保不被覆盖,这里我们使用的是 UUID。

1
2
3
4
5
6
7
8
9
10
java复制代码// 文件控件数据获取
Part part = req.getPart("headImg");
// 获取上传文件名
String realFileName = part.getSubmittedFileName();
// 获取上传文件扩展名
String ext = realFileName.substring(realFileName.lastIndexOf("."));
// 生成唯一字符串拼接文件名
String fileName = UUID.randomUUID().toString() + ext;
// 保存到磁盘上
part.write("D:/" + fileName);

3. 文件保存位置问题

​ 文件在磁盘某个位置,不在项目下,无法使用 HTTP 协议访问,所以要把用户上传的文件存放到项目中才可通过 HTTP 协议来访问,且保存的位置路径不可以写绝对路径,那么怎么办?可以通过 ServletContext 对象的 getRealPath(“项目中保存上传文件的文件夹的相对路径”) 来获取其的绝对路径。

​ 在项目的 web 目录下新建一个名为 upload 文件夹,修改 UploadServlet.java 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@WebServlet("/fileUpload")
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 文件控件数据获取
Part part = req.getPart("headImg");
// 获取上传文件名
String realFileName = part.getSubmittedFileName();
// 获取上传文件扩展名
String ext = realFileName.substring(realFileName.lastIndexOf("."));
// 生成唯一字符串拼接文件名
String fileName = UUID.randomUUID().toString() + ext;
// 获取项目下的 upload 目录的绝对路径,拼接成文件的保存路径
String realPath = getServletContext().getRealPath("/upload") +"/"+ fileName;
// 保存到磁盘上
part.write(realPath);
}
}

3.1 无法获取项目下的 upload 目录

以上方式没什么效果,原因是 IDEA 工具使用 打包 web 项目(war) 的方式来部署,所以位置有偏 差,需要还原 Web 项目的原本目录结构,以及调整部署方式。调整步骤如下:

  • WEB-INF 下 创建 classes 目录,用于存放 class 文件
  • 修改项目 class 文件输出位置到 /WEB-INF/class 中(File -> Project Structure)
    1635603531088.png
  • 调整项目部署方式为 External Source

1635603560838.png

以上操作之后可以获取到项目下的 upload 目录了,但是之前的重新部署无效了,往后可使用编译类的 方式来代替重新部署。

1635603576349.png


4. 文件类型约束问题

UploadServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@WebServlet("/fileUpload") 
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 文件控件数据获取
Part part = req.getPart("headImg");
// 判断上传的文件类型合法不
if(!part.getContentType().startsWith("img/")) {
req.setAttribute("errorMsg", "请上传图片");
req.getRequestDispatcher("/register.jsp").forward(req, resp); return;
}
String realFileName = part.getSubmittedFileName();
// 获取文件扩展名
String ext = realFileName.substring(realFileName.lastIndexOf("."));
// 生成唯一字符串拼接文件名
String fileName = UUID.randomUUID().toString() + ext;
// 获取项目下的 upload 目录的绝对路径,拼接成文件的保存路径
String realPath = getServletContext().getRealPath("/upload") +"/"+ fileName;
// 保存到磁盘上
part.write(realPath);
}
}

register.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>注册</title>
</head>
<body>
<span style="color: red">${errorMsg}</span><br/>
<form action="/fileUpload" method="post" enctype="multipart/form-data">
<p>账号:<input type="text" name="username"/></p>
<p>头像:<input type="file" name="headImg"/></p>
<input type="submit" value="注册">
</form>
</body>
</html>

5. 文件大小约束问题

文件上传限制大小可提高服务器硬盘的使用率,防止用户恶意上传文件造成服务器磁盘资源紧张。可以 通过设置 @MutipartConfifig 的属性做限制,其属性如下:

  • maxFileSize:单个上传文件大小限制,单位:bytes
  • maxRequestSize:显示请求中数据的大小,单位:bytes

四、文件下载

将服务器中的资源下载保存到用户电脑中。

1. 文件下载的简单实现

在 web 下新建 download 目录,里面提供两个资源 dog.rar 和 猫.rar

1
2
3
4
5
6
7
8
9
10
11
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>下载</title>
</head>
<body>
<h3>文件下载</h3>
<a href="/download/dog.rar">dog.rar</a><br/>
<a href="/download/猫.rar">猫.rar</a><br/>
</body>
</html>

1635605012516.png

2. 文件下载限制

​ 下载功能已经实现,但是文件放在 WEB-INF 外面不安全,用户只需要拿到下载的超链接都能够下载, 实际开发中,我们的文件通常需要用户有一定的权限才能下载,所以文件应该放在 WEB-INF 下,这样 的话,用户就不可以直接访问到了,须请求到交由 Servlet 来处理,我们就可以在其 service 方法中编 写下载限制操作。

2.1 移动下载资源WEB-INF下

image.png

2.2 修改 download.jsp 修改下载资源的链接地址 bar

1
2
3
4
5
6
7
8
9
10
11
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %> 
<html>
<head>
<title>下载</title>
</head>
<body>
<h3>文件下载</h3>
<a href="/download?fileName=dog.rar">dog.rar</a><br/>
<a href="/download?fileName=猫.rar">猫.rar</a><br/>
</body>
</html>

2.3 编写 DownloadServlet.java

根据请求传递文件名参数获取对应文件响应给浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取用户想要下载的文件名称
String fileName = req.getParameter("fileName");
//获取浏览器类型
String header = req.getHeader("User-Agent");
//根据浏览器类型设置下载文件名
//三目运算法
String mozilla = header.contains("Mozilla") ? URLEncoder.encode(fileName, "UTF-8") : new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
//设置下载文件名
resp.setHeader("Content-Disposition", "attachment;filename=" + mozilla);

//获取文件所在跟路径
String realPath = req.getServletContext().getRealPath("/WEB-INF/download/");
//使用工具类File的copy方法获取文件输入流,响应会浏览器
//Files.copy(源文件,输出)
// Path path = Paths.get("C:/", "Xmp");Path用于来表示文件路径和文件。可以有多种方法来构造一个Path对象来表示一个文件路径,或者一个文件:
//resp.getOutputStream()响应一个输出流,作用下载来用
Files.copy(Paths.get(realPath, fileName), resp.getOutputStream());


}

3. 下载文件名称问题(能使用即可)

默认情况下,Tomcat 服务器未告知浏览器文件的名称,所以需要手动设置响应头来告知浏览器文件名称,方法如下:

1
2
java复制代码// 无需记,知道需要设置即可 // 给浏览器一个推荐名称 
resp.setHeader("Content-Disposition", "attachment;filename=文件名称");

处理中文件名称的问题

  • IE 使用 URL 编码方式:URLEncoder.encode(fifileName, “UTF-8”)
  • 非 IE使用 ISO-8859-1 编码:new String (fifileName.getBytes(“UTF-8”), “ISO-8859-1”)

具体代码见上面一份

三层架构

1.三层架构介绍

​ Web 开发中的最佳实践:分层开发模式(技术层面的”分而治之”)。三层架构(3-tier architecture):通 常意义上的三层架构就是将整个业务应用划分为:表现层、业务逻辑层、数据访问层。区分层次的目的 即为了“高内聚低耦合”的思想。在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结

构。

  • 表现层(Predentation Layer):MVC,负责处理与界面交互的相关操作。
  • 业务层(Business Layer):Service,负责复杂的业务逻辑计算和判断。
  • 持久层(Persistent Layer):DAO,负责将业务逻辑数据进行持久化存储

image.png

image.png

image.png

  1. 业务层命名规范

  • 包名:
    • 公司域名倒写.模块名.service:存放业务接口代码。
    • 公司域名倒写.模块名.service.impl:存放业务层接口的实现类。
  • 类名:
    • IXxxService:业务层接口,Xxx 表示对应模型,比如操作 User 的就起名为 IUserService。
    • XxxServiceImpl:业务层接口对应的实现类,比如操作 User 的就起名为 UserServiceImpl。
    • XxxServiceTest:业务层实现的测试类。

本文转载自: 掘金

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

SpringBoot打印Mybatis执行SQL及slf4j

发表于 2021-11-28

最近在springboot项目的代码问题定位时需要知道mybatis的执行SQL,所以查了下如何配置,并顺道整理下之前一直忽略的日志内容。

1 日志框架介绍

23种设计模式中有一种模式叫门面模式。

image-20211128105530398.png

在这个结构图中,出现了两个角色:

  • 门面(Facade)角色 : 客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。
  • 子系统(SubSystem)角色 : 可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合(如上面的子系统就是由ModuleA、ModuleB、ModuleC三个类组合而成)。每个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。

使用门面模式具有以下优点:

  • 松散耦合: 门面模式松散了客户端与子系统的耦合关系,让子系统内部的模块能更容易扩展和维护。
  • 简单易用: 门面模式让子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟门面类交互就可以了。
  • 更好的划分访问层次: 通过合理使用Facade,可以帮助我们更好地划分访问的层次。有些方法是对系统外的,有些方法是系统内部使用的。把需要暴露给外部的功能集中到门面中,这样既方便客户端使用,也很好地隐藏了内部的细节。

日志框架也采用门面模式来设计,其中JCL(Jakarta Commons Logging)和SLF4J(Simple Logging Facade for Java)是日志门面,JUL(Java Util Logging)、Log4j、Logback和Log4j2则是日志实现。

参考资料:JCL、SLF4J、Log4J、Log4J2、LogBack和JUL之间的关系,你搞清楚了吗?

1.1 日志门面

日志门面中JCL最后一次更新是在2014年,目前基本都是使用SLF4J日志门面,所以只介绍SLF4J的内容。

1.1.1 SLF4J

SLF4J日志门面可以搭配JUL、Log4j、Logback和Log4j2中任何一个日志实现使用,使用SLF4J门面只需要引入slf4j-api-${project.version}.jar包即可。搭配不同日志实现也是通过在项目中引入对应的jar即可,具体如下:

  • slf4j-simple-${latest.stable.version}.jar:绑定Simple 日志实现,仅仅在很简单的项目中使用。
  • slf4j-nop-${latest.stable.version}.jar:绑定 NOP, 用于丢弃所有日志。
  • slf4j-jdk14-${latest.stable.version}.jar:绑定java.util.logging,即JDK 1.4版本的日志实现。
  • slf4j-log4j12-${latest.stable.version}.jar:绑定log4j 1.2版本的日志实现。
  • logback-classic-logback.version.jar(requireslogback−core−{logback.version}.jar (requires logback-core-logback.version.jar(requireslogback−core−{logback.version}.jar) :绑定logback日志实现。

要切换不同的日志实现,只需要替换不同的日志实现jar包就可以。以下SLF4J官网的图展示了门面和实现的绑定关系。

image-20211128115701184.png

SLF4J日志门面绑定日志实现是在编译阶段完成的,而且只能绑定一个日志实现。由于SLF4J门面不依赖任何的类加载机制,所以不存在JCL(Jakarta Commons Logging)日志门面遇到的类加载器或内存泄漏问题。

参考资料:SLF4J user manual

1.2 日志实现

目前通常使用Logback。Log4j2作为Log4j的升级版,有更好的异步日志性能,只是现在的使用面还不如Logback。

1.2.1 Log4j

Apache Log4j是一个基于Java的日志记录工具,由Ceki Gülcü首创的,现在则是Apache软件基金会的一个项目,该项目已不再维护。

1.2.2 Logback

Logback是Ceki Gülcü将Log4j捐献给Apache基金会后另外写的日志框架。作者同时写了SLF4J和Logback,所以Logback原生就支持SLF4J的API。Logback是SpringBoot选用的默认日志实现。

Logback分为3个模块:logback-core、logback-classic和logback-access。logback-core是另外两个模块的基础。logback-classic原生就实现了SLF4J的API,也是我们日常开发中会引入的依赖。logback-access用于和Tomcat、Jetty等Servlet容器集成的,提供了HTTP访问日志的功能。

相比Log4j,Logback具有以下优点:

  • 执行速度快,内存占用少。
  • 测试更加充分,虽然Log4j也进行过测试,但是和Logback的测试程度相比,不在一个量级。Logback的稳定性更有保证。
  • Logback原生就支持SLF4J的API,而Log4j要使用SLF4J的API则需要通过slf4j-log4j12-${latest.stable.version}.jar作为适应层。
  • 可以从I/O错误中快速恢复。
  • 自动移除旧版本的日志文件、异步自动压缩日志文件。

1.2.3 Log4j2

Log4j2作为Log4j的升级版,借鉴了Logback的优点并修复了Logback内在的结构缺陷。相比Logback,Logback的异步输出性能有明显优势。

2 日志框架使用

接下来介绍如何在SpringBoot项目中分别使用SLF4J+Logback和SLF4J+Log4j2。

Logback和Log4j2的日志配置的常见格式都按下图的层次来定义。Appenders中选用不同的Appender来设置将日志输出什么位置,Loggers中通过Logger指定代码中package和class的日志输出级别以及采用定义的哪些appender组件来记录日志。

image-20211128145511779.png

所使用的SpringBoot项目参考:

  • org.spring.springboot.controller.CityRestController内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@RestController
public class CityRestController {
   private final Logger LOG = LoggerFactory.getLogger(CityRestController.class);
   @Autowired
   private CityService cityService;
   @RequestMapping(value = "/api/city", method = RequestMethod.GET)
   public City findOneCity(@RequestParam(value = "cityName", required = true) String cityName) {
       LOG.debug(">>>>>>>>>>Enter Controller");
       LOG.info(">>>>>>>>>>Enter Controller");
       LOG.warn(">>>>>>>>>>Enter Controller");
       LOG.error(">>>>>>>>>>Enter Controller");
       return cityService.findCityByName(cityName);
  }
}
  • org.spring.springboot.service.impl.CityServiceImpl内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class CityServiceImpl implements CityService {
   private final Logger LOG = LoggerFactory.getLogger(CityServiceImpl.class);
   @Autowired
   private CityDao cityDao;
   public City findCityByName(String cityName) {
       LOG.debug(">>>>>>>>>>Enter Servie");
       LOG.info(">>>>>>>>>>Enter Servie");
       LOG.warn(">>>>>>>>>>Enter Servie");
       LOG.error(">>>>>>>>>>Enter Servie");
       return cityDao.findByName(cityName);
  }
}
  • org.spring.springboot.dao.CityDao内容:
1
2
3
less复制代码public interface CityDao {
   City findByName(@Param("cityName") String cityName);
}

2.1 Log4j2使用

鱿鱼SpringBoot默认采用Logback日志框架,所以要改用Log4j2,需要对pom文件做改动,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码        <!-- Spring Boot Web 依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
           <exclusions>
               <exclusion>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-logging</artifactId>
               </exclusion>
           </exclusions>
       </dependency>
​
       <!-- 引入Log4j2的starter依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-log4j2</artifactId>
       </dependency>

在SpringBoot项目中,Log4j2的日志配置文件名为log4j2.xml或者log4j2-spring.xml时,项目启动时会自动读取到里面的配置,读取顺序是:先读log4j2.xml,再读application.yaml,然后是log4j2-spring.xml,所以如果在日志配置文件中引用了application.yaml文件的内容,日志文件名不能是log4j2.xml。如果日志配置文件名既不是log4j2.xml,也不是log4j2-spring.xml,则需要在application.yaml中指定日志配置文件,配置如下:

1
2
3
yaml复制代码## 日志配置
logging:
config: classpath:log4j2-demo.xml

日志配置文件log4j2-demo.xml内容如下:

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration monitorInterval="5">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
​
<!--变量配置-->
<Properties>
   <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
   <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
   <property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss, SSS} [%thread] %-5level %logger{36} - %msg%n" />
   <!-- 定义日志存储的路径,不要配置相对路径 -->
   <property name="FILE_PATH" value="logs" />
   <property name="FILE_NAME" value="logs" />
</Properties>
​
<appenders>
   <console name="Console" target="SYSTEM_OUT">
       <!--输出日志的格式-->
       <PatternLayout pattern="${LOG_PATTERN}"/>
   </console>
​
   <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
   <File name="FileLog" fileName="${FILE_PATH}/${FILE_NAME}-all.log" append="false">
       <PatternLayout pattern="${LOG_PATTERN}"/>
   </File>
​
   <!-- 这个会打印出所有的debug及以上级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
   <RollingFile name="RollingFileDebug" fileName="${FILE_PATH}/${FILE_NAME}-debug.log" filePattern="${FILE_PATH}/${FILE_NAME}-DEBUG-%d{yyyy-MM-dd}_%i.log.gz">
       <!--只接收level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
       <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
       <PatternLayout pattern="${LOG_PATTERN}"/>
       <Policies>
           <!--interval属性用来指定多久滚动一次,默认是1 hour-->
           <TimeBasedTriggeringPolicy interval="1"/>
           <SizeBasedTriggeringPolicy size="10MB"/>
       </Policies>
       <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
       <DefaultRolloverStrategy max="15"/>
   </RollingFile>
​
   <!-- 这个会打印出所有的info及以上级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
   <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/${FILE_NAME}-info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
       <!--只接收level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
       <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
       <PatternLayout pattern="${LOG_PATTERN}"/>
       <Policies>
           <!--interval属性用来指定多久滚动一次,默认是1 hour-->
           <TimeBasedTriggeringPolicy interval="1"/>
           <SizeBasedTriggeringPolicy size="10MB"/>
       </Policies>
       <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
       <DefaultRolloverStrategy max="15"/>
   </RollingFile>
​
   <!-- 这个会打印出所有的error及以上级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
   <RollingFile name="RollingFileError" fileName="${FILE_PATH}/${FILE_NAME}-error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
       <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
       <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
       <PatternLayout pattern="${LOG_PATTERN}"/>
       <Policies>
           <!--interval属性用来指定多久滚动一次,默认是1 hour-->
           <TimeBasedTriggeringPolicy interval="1"/>
           <SizeBasedTriggeringPolicy size="10MB"/>
       </Policies>
       <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
       <DefaultRolloverStrategy max="15"/>
   </RollingFile>
</appenders>
​
<!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
<!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
   <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
   <logger name="org.mybatis" level="info" additivity="false">
       <AppenderRef ref="Console"/>
   </logger>
​
   <!--设置为debug级别以打印mybatis执行SQL-->
   <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出-->
   <Logger name="org.spring.springboot.dao" level="debug" additivity="true">
       <AppenderRef ref="Console"/>
       <appender-ref ref="FileLog"/>
       <appender-ref ref="RollingFileDebug"/>
   </Logger>
​
   <root level="info">
       <appender-ref ref="Console"/>
       <appender-ref ref="FileLog"/>
       <appender-ref ref="RollingFileInfo"/>
       <appender-ref ref="RollingFileError"/>
   </root>
</loggers>
</configuration>
​

日志打印结果:

  • 在工程目录下生成了四个日志文件,是四个Appender组件的输出结果。
  • 在logs-all日志文件中mybatis执行SQL打印了2次,这是因为日志配置文件中第二个Logger引用了FileLog Appender组件记录日志,同时Logger的additivity属性为true,表示将当前日志传递给父Logger。作为父Logger的root也引用了FileLog Appender组件,虽然root的日志级别为info,但是对于子Logger传递上来的日志,父Logger不做过滤,直接交给Appende处理。而Mybatis执行SQL日志级别为Debug,符合FileLog Appender组件的记录条件,所以Mybatis执行SQL就在logs-all日志文件记录了两次。
  • 控制台也输出两次Mybatis执行SQL,原因同上。

image-20211128152836786.png

2.2 Logback使用

Logback的使用和Log4j2类似,首先pom文件配置如下(也是Springboot项目的默认配置):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码        <!-- Spring Boot Web 依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
<!--           <exclusions>-->
<!--               <exclusion>-->
<!--                   <groupId>org.springframework.boot</groupId>-->
<!--                   <artifactId>spring-boot-starter-logging</artifactId>-->
<!--               </exclusion>-->
<!--           </exclusions>-->
       </dependency>
​
       <!-- 引入Log4j2的starter依赖 -->
<!--       <dependency>-->
<!--           <groupId>org.springframework.boot</groupId>-->
<!--           <artifactId>spring-boot-starter-log4j2</artifactId>-->
<!--       </dependency>-->

然后按照logback的配置格式创建配置文件logback-demo.xml,并修改application.yaml的配置项(文件名如果是logback.xml或者logback-spring.xml也不需要该配置项)。

1
2
3
yaml复制代码## 日志配置
logging:
config: classpath:spring-demo.xml

logback日志配置文件可参考:

看完这个不会配置 logback ,请你吃瓜!

logback 配置详解(一)——logger、root

logback最详细配置讲解,及命名规范

本文转载自: 掘金

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

Python matplotlib 绘制频谱图 复习回顾 1

发表于 2021-11-28

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

复习回顾

matplotlib 是Python专门用来绘制渲染的模块,其底层主要分为脚本层、美工层和后端。脚本层为我们提供常见图形绘制如折线、柱状、直方、饼图外,还提供一些特殊的图形如提琴、箱型、量场图等,例举往期文章。

  • 提供绘制箱型图,用于展示数据的分布情况的boxplot():matplotlib 绘制箱型图
  • 提供绘制量场图,常用于电磁场分析quiver():matplotlib 绘制量场图
  • 提供绘制展示数据分布和概率情况的violinplot():matplotlib 绘制提琴图

我们平时会遇到音视频文件质量时,会通过查看其频谱图等信息来了解音视频质量的好坏,同时频谱图应用医疗、视觉、音频检测分析等场景上。

频谱图.png

本期,我们将学习matplotlib.pyplot.specgram()相关属性的学习,let’s go~

  1. 频谱图概述

  • 什么是频谱图?

+ 频谱图表示的信号频率与能量的关系
+ 频谱图以x,y轴的波纹方式记录并画出信号在各种频率的图像
+ 频谱图可以分为三种:线性振幅谱、对数振幅谱、自功率谱
  • 频谱图应用场景

+ 频谱图常用在机械故障诊断系统中用于检测故障的部位、类型和程度等问题
+ 频谱图专门分析振动参数的主要工具
+ 频谱图对于要分在特定时段频率振幅变化情况如音频、视频等质量检测
  • 获取使用频谱图

1
2
python复制代码import matplotlib.pyplot as plt 
plt.specgram(x)
  • pyplot.specgram(x) 方法

+ specgram 计算并绘制x中数据的频谱图
+ 数据被分成NFFT长度段,并计算每个部分的频谱
+ 加窗函数window应用用于每个段,每个段的重叠量由noverlap指定
+ 频谱图使用imshow()绘制颜色图
  1. 频谱图属性

  • 设置频谱图模式

+ 关键字:mode
+ 可选值为:{'default', 'psd', 'magnitude', 'angle', 'phase'}
+ 默认为功率频谱图,magnitude为振幅频谱,angle为非展开相位频谱图,phase为展开相位频谱图
  • 设置频谱图颜色系

+ 关键字:cmap
+ 形式如:"颜色表\_r"
+ 常用的有:'Accent', 'Accent\_r', 'Blues', 'Blues\_r', 'BrBG', 'BrBG\_r', 'BuGn', 'BuGn\_r', 'BuPu', 'BuPu\_r', 'CMRmap', 'CMRmap\_r', 'Dark2', 'Dark2\_r', 'GnBu', 'GnBu\_r', 'Greens'
  • 设置频谱图窗口

+ 关键字:window
+ 默认值为:window\_hanning
+ 创建窗口的向量为:window\_hanning、window\_none、numpy.blackman、numpy.hamming
  • 设置频谱图侧边

+ 关键字:side
+ 可选值:onside|twosided
+ oneside为默认值,一侧用于真实数据,返回单边频谱
+ twoside两侧用于复杂数据,强制返回双面
  • 设置频谱图NFFT

+ 关键字:NFFT
+ 默认值为:256
+ 设置每块用于FFT的数据点数,不能应用于获取零填充
  1. 绘制频谱图步骤

  • 导入matplotlib.pyplot类
1
python复制代码import matplotlib.pyplot as plt
  • 使用numpy库里的arange()、sin()、cos()等方法准备x数据
1
2
3
4
5
6
7
8
9
10
11
python复制代码t = np.arange(0.0,20.0,dt)

s1 = np.cos(2*np.pi*100*t)

s2 = 2*np.cos(2*np.pi*400*t)

s2[t<=5]=s2[12<t]=0

nse = 0.01 * np.random.random(size=len(t))

x = s1+s2+nse
  • 调用pyplot.specgram()方法绘制频谱图
1
python复制代码plt.specgram(x,NFFT=256,Fs=1,noverlap=128)
  • 调用pyplot.show()显示频谱图
1
python复制代码plt.show()

image.png

  • 更改频谱图颜色为灰色系的,NFFT设置为1024
1
ini复制代码plt.specgram(x,NFFT=1024,Fs=1,noverlap=900,cmap="gray")

image.png

  1. 小试牛刀

我们学习了关于绘制频谱图相关属性,在实际运用中通常会结合折线图一起来看,我们来实操一下吧

  • 通过pyplot.subplots()创建Axes对象
  • Axes1对象调用plot()绘制折线图
  • Axes2对象调用specgram()方法绘制频谱图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码fig,(ax1,ax2) = plt.subplots(nrows=2)

dt= 0.0005
t = np.arange(0.0,20.0,dt)

s1 = np.cos(2*np.pi*100*t)

s2 = 2*np.cos(2*np.pi*400*t)

s2[t<=5]=s2[12<t]=0

nse = 0.01 * np.random.random(size=len(t))

x = s1+s2+nse

ax1.plot(t,x,"pink")

ax2.specgram(x,NFFT=1024,Fs=1,noverlap=900,cmap="gray")

plt.show()

image.png

总结

本期,我们对matplotlib.pyplot.specgram()方法相关属性进行学习,通常在检查音视频等频率振幅数据时使用比较多。

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

1…138139140…956

开发者博客

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