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

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


  • 首页

  • 归档

  • 搜索

基于Netty实现自定义消息通信协议(协议设计及解析应用实战

发表于 2021-11-15

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

所谓的协议,是由语法、语义、时序这三个要素组成的一种规范,通信双方按照该协议规范来实现网络数据传输,这样通信双方才能实现数据正常通信和解析。

由于不同的中间件在功能方面有一定差异,所以其实应该是没有一种标准化协议来满足不同差异化需求,因此很多中间件都会定义自己的通信协议,另外通信协议可以解决粘包和拆包问题。

在本篇文章中,我们来实现一个自定义消息协议。

自定义协议的要素

自定义协议,那这个协议必须要有组成的元素,

  • 魔数: 用来判断数据包的有效性
  • 版本号: 可以支持协议升级
  • 序列化算法: 消息正文采用什么样的序列化和反序列化方式,比如json、protobuf、hessian等
  • 指令类型:也就是当前发送的是一个什么类型的消息,像zookeeper中,它传递了一个Type
  • 请求序号: 基于双工协议,提供异步能力,也就是收到的异步消息需要找到前面的通信请求进行响应处理
  • 消息长度
  • 消息正文

协议定义

1
txt复制代码sessionId | reqType | Content-Length | Content |

其中Version,Content-Length,SessionId就是Header信息,Content就是交互的主体。

定义项目结构以及引入包

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

项目结构如图4-1所示:

  • netty-message-mic : 表示协议模块。
  • netty-message-server :表示nettyserver。

图4-1

  • 引入log4j.properties

在nettyMessage-mic中,包的结构如下。

image-20210831103346370

定义Header

表示消息头

1
2
3
4
5
6
7
java复制代码@Data
public class Header{
private long sessionId; //会话id : 占8个字节
private byte type; //消息类型: 占1个字节

private int length; //消息长度 : 占4个字节
}

定义MessageRecord

表示消息体

1
2
3
4
5
6
java复制代码@Data
public class MessageRecord{

private Header header;
private Object body;
}

OpCode

定义操作类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public enum OpCode {

BUSI_REQ((byte)0),
BUSI_RESP((byte)1),
PING((byte)3),
PONG((byte)4);

private byte code;

private OpCode(byte code) {
this.code=code;
}

public byte code(){
return this.code;
}
}

定义编解码器

分别定义对该消息协议的编解码器

MessageRecordEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
public class MessageRecordEncoder extends MessageToByteEncoder<MessageRecord> {

@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageRecord record, ByteBuf byteBuf) throws Exception {
log.info("===========开始编码Header部分===========");
Header header=record.getHeader();
byteBuf.writeLong(header.getSessionId()); //保存8个字节的sessionId
byteBuf.writeByte(header.getType()); //写入1个字节的请求类型

log.info("===========开始编码Body部分===========");
Object body=record.getBody();
if(body!=null){
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(body);
byte[] bytes=bos.toByteArray();
byteBuf.writeInt(bytes.length); //写入消息体长度:占4个字节
byteBuf.writeBytes(bytes); //写入消息体内容
}else{
byteBuf.writeInt(0); //写入消息长度占4个字节,长度为0
}
}
}

MessageRecordDecode

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
java复制代码@Slf4j
public class MessageRecordDecode extends ByteToMessageDecoder {

@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
MessageRecord record=new MessageRecord();
Header header=new Header();
header.setSessionId(byteBuf.readLong()); //读取8个字节的sessionid
header.setType(byteBuf.readByte()); //读取一个字节的操作类型
record.setHeader(header);
//如果byteBuf剩下的长度还有大于4个字节,说明body不为空
if(byteBuf.readableBytes()>4){
int length=byteBuf.readInt(); //读取四个字节的长度
header.setLength(length);
byte[] contents=new byte[length];
byteBuf.readBytes(contents,0,length);
ByteArrayInputStream bis=new ByteArrayInputStream(contents);
ObjectInputStream ois=new ObjectInputStream(bis);
record.setBody(ois.readObject());
list.add(record);
log.info("序列化出来的结果:"+record);
}else{
log.error("消息内容为空");
}
}
}

测试协议的解析和编码

EmbeddedChannel是netty专门改进针对ChannelHandler的单元测试而提供的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class CodesMainTest {
public static void main( String[] args ) throws Exception {
EmbeddedChannel channel=new EmbeddedChannel(
new LoggingHandler(),
new MessageRecordEncoder(),
new MessageRecordDecode());
Header header=new Header();
header.setSessionId(123456);
header.setType(OpCode.PING.code());
MessageRecord record=new MessageRecord();
record.setBody("Hello World");
record.setHeader(header);
channel.writeOutbound(record);

ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
new MessageRecordEncoder().encode(null,record,buf);
channel.writeInbound(buf);
}
}

编码包分析

运行上述代码后,会得到下面的一个信息

1
2
3
4
5
6
txt复制代码         +-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 00 00 01 e2 40 03 00 00 00 12 ac ed 00 |.......@........|
|00000010| 05 74 00 0b 48 65 6c 6c 6f 20 57 6f 72 6c 64 |.t..Hello World |
+--------+-------------------------------------------------+----------------+

按照协议规范:

  • 前面8个字节表示sessionId
  • 一个字节表示请求类型
  • 4个字节表示长度
  • 后面部分内容表示消息体

测试粘包和半包问题

通过slice方法进行拆分,得到两个包。

ByteBuf中提供了一个slice方法,这个方法可以在不做数据拷贝的情况下对原始ByteBuf进行拆分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public class CodesMainTest {
public static void main( String[] args ) throws Exception {
//EmbeddedChannel是netty专门针对ChannelHandler的单元测试而提供的类。可以通过这个类来测试channel输入入站和出站的实现
EmbeddedChannel channel=new EmbeddedChannel(
//解决粘包和半包问题
// new LengthFieldBasedFrameDecoder(2048,10,4,0,0),
new LoggingHandler(),
new MessageRecordEncoder(),
new MessageRecordDecode());
Header header=new Header();
header.setSessionId(123456);
header.setType(OpCode.PING.code());
MessageRecord record=new MessageRecord();
record.setBody("Hello World");
record.setHeader(header);
channel.writeOutbound(record);

ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
new MessageRecordEncoder().encode(null,record,buf);

//*********模拟半包和粘包问题************//
//把一个包通过slice拆分成两个部分
ByteBuf bb1=buf.slice(0,7); //获取前面7个字节
ByteBuf bb2=buf.slice(7,buf.readableBytes()-7); //获取后面的字节
bb1.retain();

channel.writeInbound(bb1);
channel.writeInbound(bb2);
}
}

运行上述代码会得到如下异常, readerIndex(0) +length(8)表示要读取8个字节,但是只收到7个字节,所以直接报错。

1
2
3
4
5
6
7
8
txt复制代码2021-08-31 15:53:01,385 [io.netty.handler.logging.LoggingHandler]-[DEBUG] [id: 0xembedded, L:embedded - R:embedded] READ: 7B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 00 00 01 e2 |....... |
+--------+-------------------------------------------------+----------------+
2021-08-31 15:53:01,397 [io.netty.handler.logging.LoggingHandler]-[DEBUG] [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
Exception in thread "main" io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(0) + length(8) exceeds writerIndex(7): UnpooledSlicedByteBuf(ridx: 0, widx: 7, cap: 7/7, unwrapped: PooledUnsafeDirectByteBuf(ridx: 0, widx: 31, cap: 256))

解决拆包问题

LengthFieldBasedFrameDecoder是长度域解码器,它是解决拆包粘包最常用的解码器,基本上能覆盖大部分基于长度拆包的场景。其中开源的消息中间件RocketMQ就是使用该解码器进行解码的。

首先来说明一下该解码器的核心参数

  • lengthFieldOffset,长度字段的偏移量,也就是存放长度数据的起始位置
  • lengthFieldLength,长度字段锁占用的字节数
  • lengthAdjustment,在一些较为复杂的协议设计中,长度域不仅仅包含消息的长度,还包含其他数据比如版本号、数据类型、数据状态等,这个时候我们可以使用lengthAdjustment进行修正,它的值=包体的长度值-长度域的值
  • initialBytesToStrip,解码后需要跳过的初始字节数,也就是消息内容字段的起始位置
  • lengthFieldEndOffset,长度字段结束的偏移量, 该属性的值=lengthFieldOffset+lengthFieldLength
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public class CodesMainTest {
public static void main( String[] args ) throws Exception {
EmbeddedChannel channel=new EmbeddedChannel(
//解决粘包和半包问题
new LengthFieldBasedFrameDecoder(1024,
9,4,0,0),
new LoggingHandler(),
new MessageRecordEncoder(),
new MessageRecordDecode());
Header header=new Header();
header.setSessionId(123456);
header.setType(OpCode.PING.code());
MessageRecord record=new MessageRecord();
record.setBody("Hello World");
record.setHeader(header);
channel.writeOutbound(record);

ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
new MessageRecordEncoder().encode(null,record,buf);

//*********模拟半包和粘包问题************//
//把一个包通过slice拆分成两个部分
ByteBuf bb1=buf.slice(0,7);
ByteBuf bb2=buf.slice(7,buf.readableBytes()-7);
bb1.retain();

channel.writeInbound(bb1);
channel.writeInbound(bb2);
}
}

添加一个长度解码器,就解决了拆包带来的问题。运行结果如下

1
2
java复制代码2021-08-31 16:09:35,115 [com.netty.example.codec.MessageRecordDecode]-[INFO] 序列化出来的结果:MessageRecord(header=Header(sessionId=123456, type=3, length=18), body=Hello World)
2021-08-31 16:09:35,116 [io.netty.handler.logging.LoggingHandler]-[DEBUG] [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE

基于自定义消息协议通信

下面我们把整个通信过程编写完整,代码结构如图4-2所示.

image-20210831175056500

图4-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
java复制代码@Slf4j
public class ProtocolServer {

public static void main(String[] args){
EventLoopGroup boss = new NioEventLoopGroup();
//2 用于对接受客户端连接读写操作的线程工作组
EventLoopGroup work = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(boss, work) //绑定两个工作线程组
.channel(NioServerSocketChannel.class) //设置NIO的模式
// 初始化绑定服务通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline()
.addLast(
new LengthFieldBasedFrameDecoder(1024,
9,4,0,0))
.addLast(new MessageRecordEncoder())
.addLast(new MessageRecordDecode())
.addLast(new ServerHandler());
}
});
ChannelFuture cf= null;
try {
cf = b.bind(8080).sync();
log.info("ProtocolServer start success");
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
work.shutdownGracefully();
boss.shutdownGracefully();
}
}
}

ServerHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
public class ServerHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
MessageRecord messageRecord=(MessageRecord)msg;
log.info("server receive message:"+messageRecord);
MessageRecord res=new MessageRecord();
Header header=new Header();
header.setSessionId(messageRecord.getHeader().getSessionId());
header.setType(OpCode.BUSI_RESP.code());
String message="Server Response Message!";
res.setBody(message);
header.setLength(message.length());
ctx.writeAndFlush(res);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("服务器读取数据异常");
super.exceptionCaught(ctx, cause);
ctx.close();
}
}

客户端开发

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
java复制代码public class ProtocolClient {

public static void main(String[] args) {
//创建工作线程组
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,
9,4,0,0))
.addLast(new MessageRecordEncoder())
.addLast(new MessageRecordDecode())
.addLast(new ClientHandler());

}
});
// 发起异步连接操作
try {
ChannelFuture future = b.connect(new InetSocketAddress("localhost", 8080)).sync();
Channel c = future.channel();
for (int i = 0; i < 500; i++) {
MessageRecord message = new MessageRecord();
Header header = new Header();
header.setSessionId(10001);
header.setType((byte) OpCode.BUSI_REQ.code());
message.setHeader(header);
String context="我是请求数据"+i;
header.setLength(context.length());
message.setBody(context);
c.writeAndFlush(message);
}
//closeFuture().sync()就是让当前线程(即主线程)同步等待Netty server的close事件,Netty server的channel close后,主线程才会继续往下执行。closeFuture()在channel close的时候会通知当前线程。
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}

ClientHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Slf4j
public class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
MessageRecord record=(MessageRecord)msg;
log.info("Client Receive message:"+record);
super.channelRead(ctx, msg);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
ctx.close();
}
}

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

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

C语言基础,来喽! C 中的数据

发表于 2021-11-15

前言

C 语言是一门抽象的、面向过程的语言,C 语言广泛应用于底层开发,C 语言在计算机体系中占据着不可替代的作用,可以说 C 语言是编程的基础,也就是说,不管你学习任何语言,都应该把 C 语言放在首先要学的位置上。下面这张图更好的说明 C 语言的重要性

01
可以看到,C 语言是一种底层语言,是一种系统层级的语言,操作系统就是使用 C 语言来编写的,比如 Windows、Linux、UNIX 。如果说其他语言是光鲜亮丽的外表,那么 C 语言就是灵魂,永远那么朴实无华。

原文链接:C 语言基础,来喽!

C 语言特性

那么,既然 C 语言这么重要,它有什么值得我们去学的地方呢?我们不应该只因为它重要而去学,我们更在意的是学完我们能学会什么,能让我们获得什么。

C 语言的设计

C 语言是 1972 年,由贝尔实验室的丹尼斯·里奇(Dennis Ritch)和肯·汤普逊(Ken Thompson)在开发 UNIX 操作系统时设计了C语言。C 语言是一门流行的语言,它把计算机科学理论和工程实践理论完美的融合在一起,使用户能够完成模块化的编程和设计。

计算机科学理论:简称 CS、是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。

C 语言具有高效性

C 语言是一门高效性语言,它被设计用来充分发挥计算机的优势,因此 C 语言程序运行速度很快,C 语言能够合理了使用内存来获得最大的运行速度

C 语言具有可移植性

C 语言是一门具有可移植性的语言,这就意味着,对于在一台计算机上编写的 C 语言程序可以在另一台计算机上轻松地运行,从而极大的减少了程序移植的工作量。

C 语言特点

  • C 语言是一门简洁的语言,因为 C 语言设计更加靠近底层,因此不需要众多 Java 、C# 等高级语言才有的特性,程序的编写要求不是很严格。
  • C 语言具有结构化控制语句,C 语言是一门结构化的语言,它提供的控制语句具有结构化特征,如 for 循环、if⋯ else 判断语句和 switch 语句等。
  • C 语言具有丰富的数据类型,不仅包含有传统的字符型、整型、浮点型、数组类型等数据类型,还具有其他编程语言所不具备的数据类型,比如指针。
  • C 语言能够直接对内存地址进行读写,因此可以实现汇编语言的主要功能,并可直接操作硬件。
  • C 语言速度快,生成的目标代码执行效率高。

下面让我们通过一个简单的示例来说明一下 C 语言

入门级 C 语言程序

下面我们来看一个很简单的 C 语言程序,我觉得工具无所谓大家用着顺手就行。

第一个 C 语言程序

1
2
3
4
5
6
7
8
9
10
11
c复制代码#include <stdio.h>

int main(int argc, const char * argv[]) {
printf("Hello, World!\n");

printf("my Name is cxuan \n")

printf("number = %d \n", number);

return 0;
}

你可能不知道这段代码是什么意思,不过别着急,我们先运行一下看看结果。

这段程序输出了 Hello,World! 和 My Name is cxuan,下面我们解释一下各行代码的含义。

首先,第一行的 #include <stdio.h>, 这行代码包含另一个文件,这一行告诉编译器把 stdio.h 的内容包含在当前程序中。 stdio.h 是 C 编译器软件包的标准部分,它能够提供键盘输入和显示器输出。

什么是 C 标准软件包?C 是由 Dennis M 在1972年开发的通用,过程性,命令式计算机编程语言。C标准库是一组 C 语言内置函数,常量和头文件,例如<stdio.h>,<stdlib.h>,<math.h>等。此库将用作 C 程序员的参考手册。

我们后面会介绍 stdio.h ,现在你知道它是什么就好。

在 stdio.h 下面一行代码就是 main 函数。

C 程序能够包含一个或多个函数,函数是 C 语言的根本,就和方法是 Java 的基本构成一样。main() 表示一个函数名,int 表示的是 main 函数返回一个整数。void 表明 main() 不带任何参数。这些我们后面也会详细说明,只需要记住 int 和 void 是标准 ANSI C 定义 main() 的一部分(如果使用 ANSI C 之前的编译器,请忽略 void)。

然后是 /*一个简单的 C 语言程序*/ 表示的是注释,注释使用 /**/ 来表示,注释的内容在两个符号之间。这些符号能够提高程序的可读性。

注意:注释只是为了帮助程序员理解代码的含义,编译器会忽略注释

下面就是 { ,这是左花括号,它表示的是函数体的开始,而最后的右花括号 } 表示函数体的结束。 { } 中间是书写代码的地方,也叫做代码块。

int number 表示的是将会使用一个名为 number 的变量,而且 number 是 int 整数类型。

number = 11 表示的是把值 11 赋值给 number 的变量。

printf(Hello,world!\n); 表示调用一个函数,这个语句使用 printf() 函数,在屏幕上显示 Hello,world , printf() 函数是 C 标准库函数中的一种,它能够把程序运行的结果输出到显示器上。而代码 \n 表示的是 换行,也就是另起一行,把光标移到下一行。

然后接下来的一行 printf() 和上面一行是一样的,我们就不多说了。最后一行 printf() 有点意思,你会发现有一个 %d 的语法,它的意思表示的是使用整形输出字符串。

代码块的最后一行是 return 0,它可以看成是 main 函数的结束,最后一行是代码块 } ,它表示的是程序的结束。

好了,我们现在写完了第一个 C 语言程序,有没有对 C 有了更深的认识呢?肯定没有。。。这才哪到哪,继续学习吧。

现在,我们可以归纳为 C 语言程序的几个组成要素,如下图所示

03
C 语言执行流程


C 语言程序成为高级语言的原因是它能够读取并理解人们的思想。然而,为了能够在系统中运行 hello.c 程序,则各个 C 语句必须由其他程序转换为一系列低级机器语言指令。这些指令被打包作为可执行对象程序,存储在二进制磁盘文件中。目标程序也称为可执行目标文件。

在 UNIX 系统中,从源文件到对象文件的转换是由编译器执行完成的。

1
shell复制代码gcc -o hello hello.c

gcc 编译器驱动从源文件读取 hello.c ,并把它翻译成一个可执行文件 hello。这个翻译过程可用如下图来表示

04
这就是一个完整的 hello world 程序执行过程,会涉及几个核心组件:预处理器、编译器、汇编器、连接器,下面我们逐个击破。

  • 预处理阶段(Preprocessing phase),预处理器会根据开始的 # 字符,修改源 C 程序。#include <stdio.h> 命令就会告诉预处理器去读系统头文件 stdio.h 中的内容,并把它插入到程序作为文本。然后就得到了另外一个 C 程序hello.i,这个程序通常是以 .i为结尾。
  • 然后是 编译阶段(Compilation phase),编译器会把文本文件 hello.i 翻译成文本hello.s,它包括一段汇编语言程序(assembly-language program)。
  • 编译完成之后是汇编阶段(Assembly phase),这一步,汇编器 as会把 hello.s 翻译成机器指令,把这些指令打包成可重定位的二进制程序(relocatable object program)放在 hello.c 文件中。它包含的 17 个字节是函数 main 的指令编码,如果我们在文本编辑器中打开 hello.o 将会看到一堆乱码。
  • 最后一个是链接阶段(Linking phase),我们的 hello 程序会调用 printf 函数,它是 C 编译器提供的 C 标准库中的一部分。printf 函数位于一个叫做 printf.o文件中,它是一个单独的预编译好的目标文件,而这个文件必须要和我们的 hello.o 进行链接,连接器(ld) 会处理这个合并操作。结果是,hello 文件,它是一个可执行的目标文件(或称为可执行文件),已准备好加载到内存中并由系统执行。

你需要理解编译系统做了什么

对于上面这种简单的 hello 程序来说,我们可以依赖编译系统(compilation system)来提供一个正确和有效的机器代码。然而,对于我们上面讲的程序员来说,编译器有几大特征你需要知道

  • 优化程序性能(Optimizing program performance),现代编译器是一种高效的用来生成良好代码的工具。对于程序员来说,你无需为了编写高质量的代码而去理解编译器内部做了什么工作。然而,为了编写出高效的 C 语言程序,我们需要了解一些基本的机器码以及编译器将不同的 C 语句转化为机器代码的过程。
  • 理解链接时出现的错误(Understanding link-time errors),在我们的经验中,一些非常复杂的错误大多是由链接阶段引起的,特别是当你想要构建大型软件项目时。
  • 避免安全漏洞(Avoiding security holes),近些年来,缓冲区溢出(buffer overflow vulnerabilities)是造成网络和 Internet 服务的罪魁祸首,所以我们有必要去规避这种问题。

系统硬件组成

为了理解 hello 程序在运行时发生了什么,我们需要首先对系统的硬件有一个认识。下面这是一张 Intel 系统产品的模型,我们来对其进行解释

05

  • 总线(Buses):在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。通常总线被设计成传送定长的字节块,也就是 字(word)。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。现在大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。

06

  • I/O 设备(I/O Devices):Input/Output 设备是系统和外部世界的连接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。

每个I/O 设备连接 I/O 总线都被称为控制器(controller) 或者是 适配器(Adapter)。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备本身或者系统的主印制板电路(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论组织形式如何,它们的最终目的都是彼此交换信息。

  • 主存(Main Memory),主存是一个临时存储设备,而不是永久性存储,磁盘是 永久性存储 的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 DRAM(dynamic random access memory) 动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它唯一的地址编号,从 0 开始。一般来说,组成程序的每条机器指令都由不同数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。比如,在 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 需要 4 个字节,而 long 和 double 需要 8 个字节。
  • 处理器(Processor),CPU(central processing unit) 或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。

从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操作。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操作,然后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(比如 jmp 指令就不会顺序读取)

下面是 CPU 可能执行简单操作的几个步骤

  • 加载(Load):从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容
  • 存储(Store):将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容
  • 操作(Operate):把两个寄存器的内容复制到 ALU(Arithmetic logic unit) 。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。

算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。

  • 跳转(jump):从指令中抽取一个字,把这个字复制到程序计数器(PC) 中,覆盖原来的值

剖析 hello 程序的执行过程

前面我们简单的介绍了一下计算机的硬件的组成和操作,现在我们正式介绍运行示例程序时发生了什么,我们会从宏观的角度进行描述,不会涉及到所有的技术细节

刚开始时,shell 程序执行它的指令,等待用户键入一个命令。当我们在键盘上输入了 ./hello 这几个字符时,shell 程序将字符逐一读入寄存器,再把它放到内存中,如下图所示

07
当我们在键盘上敲击回车键的时候,shell 程序就知道我们已经结束了命令的输入。然后 shell 执行一系列指令来加载可执行的 hello 文件,这些指令将目标文件中的代码和数据从磁盘复制到主存。

利用 DMA(Direct Memory Access) 技术可以直接将磁盘中的数据复制到内存中,如下

08
一旦目标文件中 hello 中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 hello,world\n 字符串中的字节从主存复制到寄存器文件,再从寄存器中复制到显示设备,最终显示在屏幕上。如下所示

09

高速缓存是关键

上面我们介绍完了一个 hello 程序的执行过程,系统花费了大量时间把信息从一个地方搬运到另外一个地方。hello 程序的机器指令最初存储在磁盘上。当程序加载后,它们会拷贝到主存中。当 CPU 开始运行时,指令又从内存复制到 CPU 中。同样的,字符串数据 hello,world \n 最初也是在磁盘上,它被复制到内存中,然后再到显示器设备输出。从程序员的角度来看,这种复制大部分是开销,这减慢了程序的工作效率。因此,对于系统设计来说,最主要的一个工作是让程序运行的越来越快。

由于物理定律,较大的存储设备要比较小的存储设备慢。而由于寄存器和内存的处理效率在越来越大,所以针对这种差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory, 简称为 cache 高速缓存),作为暂时的集结区域,存放近期可能会需要的信息。如下图所示

10
图中我们标出了高速缓存的位置,位于高速缓存中的 L1高速缓存容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。容量更大的 L2 高速缓存通过一条特殊的总线链接 CPU,虽然 L2 缓存比 L1 缓存慢 5 倍,但是仍比内存要哦快 5 - 10 倍。L1 和 L2 是使用一种静态随机访问存储器(SRAM) 的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L1、L2 和 L3。系统可以获得一个很大的存储器,同时访问速度也更快,原因是利用了高速缓存的 局部性原理。

Again:入门程序细节

现在,我们来探讨一下入门级程序的细节,由浅入深的来了解一下 C 语言的特性。

#include<stdio.h>

我们上面说到,#include<stdio.h> 是程序编译之前要处理的内容,称为编译预处理命令。

预处理命令是在编译之前进行处理。预处理程序一般以 # 号开头。

所有的 C 编译器软件包都提供 stdio.h 文件。该文件包含了给编译器使用的输入和输出函数,比如 println() 信息。该文件名的含义是标准输入/输出 头文件。通常,在 C 程序顶部的信息集合被称为 头文件(header)。

C 的第一个标准是由 ANSI 发布的。虽然这份文档后来被国际标准化组织(ISO)采纳并且 ISO 发布的修订版也被 ANSI 采纳了,但名称 ANSI C(而不是 ISO C) 仍被广泛使用。一些软件开发者使用ISO C,还有一些使用 Standard C。

C 标准库

除了 <sdtio.h> 外,C 标准库还包括下面这些头文件

11
<assert.h>

提供了一个名为 assert 的关键字,它用于验证程序作出的假设,并在假设为假输出诊断消息。

<ctype.h>

C 标准库的 ctype.h 头文件提供了一些函数,可以用于测试和映射字符。

这些字符接受 int 作为参数,它的值必须是 EOF 或者是一个无符号字符

EOF是一个计算机术语,为 End Of File 的缩写,在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束。

<errno.h>

C 标准库的 errno.h 头文件定义了整数变量 errno,它是通过系统调用设置的,这些库函数表明了什么发生了错误。

<float.h>

C 标准库的 float.h 头文件包含了一组与浮点值相关的依赖于平台的常量。

<limits.h>

limits.h 头文件决定了各种变量类型的各种属性。定义在该头文件中的宏限制了各种变量类型(比如 char、int 和 long)的值。

<locale.h>

locale.h 头文件定义了特定地域的设置,比如日期格式和货币符号

<math.h>

math.h 头文件定义了各种数学函数和一个宏。在这个库中所有可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。

<setjmp.h>

setjmp.h 头文件定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则。

<signal.h>

signal.h 头文件定义了一个变量类型 sig_atomic_t、两个函数调用和一些宏来处理程序执行期间报告的不同信号。

<stdarg.h>

stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。

<stddef.h>

stddef .h 头文件定义了各种变量类型和宏。这些定义中的大部分也出现在其它头文件中。

<stdlib.h>

stdlib .h 头文件定义了四个变量类型、一些宏和各种通用工具函数。

<string.h>

string .h 头文件定义了一个变量类型、一个宏和各种操作字符数组的函数。

<time.h>

time.h 头文件定义了四个变量类型、两个宏和各种操作日期和时间的函数。

main() 函数

main 函数听起来像是调皮捣蛋的孩子故意给方法名起一个 主要的 方法,来告诉他人他才是这个世界的中心。但事实却不是这样,而 main() 方法确实是世界的中心。

C 语言程序一定从 main() 函数开始执行,除了 main() 函数外,你可以随意命名其他函数。通常,main 后面的 () 中表示一些传入信息,我们上面的那个例子中没有传递信息,因为圆括号中的输入是 void 。

除了上面那种写法外,还有两种 main 方法的表示方式,一种是 void main(){} ,一种是 int main(int argc, char* argv[]) {}

  • void main() 声明了一个带有不确定参数的构造方法
  • int main(int argc, char* argv[]) {} 其中的 argc 是一个非负值,表示从运行程序的环境传递到程序的参数数量。它是指向 argc + 1 指针数组的第一个元素的指针,其中最后一个为null,而前一个(如果有的话)指向表示从主机环境传递给程序的参数的字符串。 如果argv [0]不是空指针(或者等效地,如果argc> 0),则指向表示程序名称的字符串,如果在主机环境中无法使用程序名称,则该字符串为空。

注释

在程序中,使用 /**/ 的表示注释,注释对于程序来说没有什么实际用处,但是对程序员来说却非常有用,它能够帮助我们理解程序,也能够让他人看懂你写的程序,我们在开发工作中,都非常反感不写注释的人,由此可见注释非常重要。

C 语言注释的好处是,它可以放在任意地方,甚至代码在同一行也没关系。较长的注释可以多行表示,我们使用 /**/ 表示多行注释,而 // 只表示的是单行注释。下面是几种注释的表示形式

1
2
3
4
5
6
7
8
9
10
11
c复制代码// 这是一个单行注释

/* 多行注释用一行表示 */

/*
多行注释用多行表示
多行注释用多行表示
多行注释用多行表示
多行注释用多行表示

*/

函数体

在头文件、main 方法后面的就是函数体(注释一般不算),函数体就是函数的执行体,是你编写大量代码的地方。

变量声明

在我们入门级的代码中,我们声明了一个名为 number 的变量,它的类型是 int,这行代码叫做 声明,声明是 C 语言最重要的特性之一。这个声明完成了两件事情:定义了一个名为 number 的变量,定义 number 的具体类型。

int 是 C 语言的一个 关键字(keyword),表示一种基本的 C 语言数据类型。关键字是用于语言定义的。不能使用关键字作为变量进行定义。

示例中的 number 是一个 标识符(identifier),也就是一个变量、函数或者其他实体的名称。

###变量赋值

在入门例子程序中,我们声明了一个 number 变量,并为其赋值为 11,赋值是 C 语言的基本操作之一。这行代码的意思就是把值 1 赋给变量 number。在执行 int number 时,编译器会在计算机内存中为变量 number 预留空间,然后在执行这行赋值表达式语句时,把值存储在之前预留的位置。可以给 number 赋不同的值,这就是 number 之所以被称为 变量(variable) 的原因。

12

printf 函数

在入门例子程序中,有三行 printf(),这是 C 语言的标准函数。圆括号中的内容是从 main 函数传递给 printf 函数的。参数分为两种:实际参数(actual argument) 和 形式参数(formal parameters)。我们上面提到的 printf 函数括号中的内容,都是实参。

return 语句

在入门例子程序中,return 语句是最后一条语句。int main(void) 中的 int 表明 main() 函数应返回一个整数。有返回值的 C 函数要有 return 语句,没有返回值的程序也建议大家保留 return 关键字,这是一种好的习惯或者说统一的编码风格。

分号

在 C 语言中,每一行的结尾都要用 ; 进行结束,它表示一个语句的结束,如果忘记或者会略分号会被编译器提示错误。

关键字

下面是 C 语言中的关键字,C 语言的关键字一共有 32 个,根据其作用不同进行划分

数据类型关键字

数据类型的关键字主要有 12 个,分别是

  • char: 声明字符型变量或函数
  • double: 声明双精度变量或函数
  • float: 声明浮点型变量或函数
  • int : 声明整型变量或函数
  • long: 声明长整型变量或函数
  • short : 声明短整型变量或函数
  • signed : 声明有符号类型变量或函数
  • _Bool: 声明布尔类型
  • _Complex :声明复数
  • _Imaginary: 声明虚数
  • unsigned : 声明无符号类型变量或函数
  • void : 声明函数无返回值或无参数,声明无类型指针

控制语句关键字

控制语句循环的关键字也有 12 个,分别是

循环语句

  • for : for 循环,使用的最多
  • do :循环语句的前提条件循环体
  • while:循环语句的循环条件
  • break : 跳出当前循环
  • continue:结束当前循环,开始下一轮循环

条件语句

  • if:条件语句的判断条件
  • else : 条件语句的否定分支,与 if 连用
  • goto: 无条件跳转语句

开关语句

  • switch: 用于开关语句
  • case:开关语句的另外一种分支
  • default : 开关语句中的其他分支

返回语句

retur :子程序返回语句(可以带参数,也看不带参数)

存储类型关键字

  • auto : 声明自动变量 一般不使用
  • extern : 声明变量是在其他文件正声明(也可以看做是引用变量)
  • register : 声明寄存器变量
  • static: 声明静态变量

其他关键字

  • const: 声明只读变量
  • sizeof : 计算数据类型长度
  • typedef: 用以给数据类型取别名
  • volatile : 说明变量在程序执行中可被隐含地改变

C 中的数据

我们在了解完上面的入门例子程序后,下面我们就要全面认识一下 C 语言程序了,首先我们先来认识一下 C 语言最基本的变量与常量。

变量和常量

变量和常量是程序处理的两种基本对象。

有些数据类型在程序使用之前就已经被设定好了,在整个过程中没有变化(这段话描述不准确,但是为了通俗易懂,暂且这么描述),这种数据被称为常量(constant)。另外一种数据类型在程序执行期间可能会发生改变,这种数据类型被称为 变量(variable)。例如 int number 就是一个变量,而3.1415 就是一个常量,因为 int number 一旦声明出来,你可以对其任意赋值,而 3.1415 一旦声明出来,就不会再改变。

变量名

有必要在聊数据类型之前先说一说变量名的概念。变量名是由字母和数字组成的序列,第一个字符必须是字母。在变量名的命名过程中,下划线 _ 被看作字母,下划线一般用于名称较长的变量名,这样能够提高程序的可读性。变量名通常不会以下划线来开头。在 C 中,大小写是有区别的,也就是说,a 和 A 完全是两个不同的变量。一般变量名使用小写字母,符号常量(#define 定义的)全都使用大写。选择变量名的时候,尽量能够从字面上描述出变量的用途,切忌起这种 abc 毫无意义的变量。

还需要注意一般局部变量都会使用较短的变量名,外部变量使用较长的名字。

数据类型

在了解数据类型之前,我们需要先了解一下这些概念 位、字节和字。

位、字节和字都是对计算机存储单元的描述。在计算机世界中,最小的单元是 位(bit),一个位就表示一个 0 或 1,一般当你的小伙伴问你的电脑是 xxx 位,常见的有 32 位或者 64 位,这里的位就指的是比特,比特就是 bit 的中文名称,所以这里的 32 位或者 64 位指的就是 32 bit 或者 64 bit。字节是基本的存储单元,基本存储单元说的是在计算机中都是按照字节来存储的,一个字节等于 8 位,即 1 byte = 8 bit。字是自然存储单位,在现代计算机中,一个字等于 2 字节。

C 语言的数据类型有很多,下面我们就来依次介绍一下。

整型

C 语言中的整型用 int 来表示,可以是正整数、负整数或零。在不同位数的计算机中其取值范围也不同。不过在 32 位和 64 位计算机中,int 的取值范围是都是 2^32 ,也就是 -2147483648 ~ +2147483647,无符号类型的取值范围是 0 ~ 4294967295。

整型以二进制整数存储,分为有符号数和无符号数两种形态,有符号数可以存储正整数、负整数和零;无符号只能存储正整数和零。

可以使用 printf 打印出来 int 类型的值,如下代码所示。

1
2
3
4
5
6
7
8
9
c复制代码#include <stdio.h> 
int main(){
int a = -5;
printf("%d\n",a);

unsigned int b = 6;
printf("%d\n",b);

}

C 语言还提供 3 个附属关键字修饰整数类型,即 short、long 和 unsigned。

  • short int 类型(或者简写为 short)占用的存储空间可能比 int 类型少,适合用于数值较小的场景。
  • long int 或者 long 占用的存储空间可能比 int 类型多,适合用于数值较大的场景。
  • long long int 或者 long long(C99 加入)占用的存储空间比 long 多,适用于数值更大的场合,至少占用 64 位,与 int 类似,long long 也是有符号类型。
  • unsigned int 或 unsigned 只用于非负值的场景,这种类型的取值范围有所不同,比如 16 位的 unsigned int 表示的范围是 0 ~ 65535 ,而不是 -32768 ~ 32767。
  • 在 C90 标准中,添加了 unsigned long int 或者 unsigned long 和 unsigned short int 或 unsigned short 类型,在 C99 中又添加了 unsigned long long int 或者 unsigned long long 。
  • 在任何有符号类型前面加 signed ,可强调使用有符号类型的意图。比如 short、short int、signed short、signed short int 都表示一种类型。

比如上面这些描述可以用下面这些代码来声明:

1
2
3
4
5
6
7
8
9
c复制代码long int lia;
long la;
long long lla;
short int sib;
short sb;
unsigned int uic;
unsigned uc;
unsigned long uld;
unsigned short usd;

这里需要注意一点,unsigned 定义的变量,按照 printf 格式化输出时,是能够显示负值的,为什么呢?不是 unsigned 修饰的值不能是负值啊,那是因为 unsigned 修饰的变量,在计算时会有用,输出没什么影响,这也是 cxuan 刚开始学习的时候踩的坑。

我们学过 Java 的同学刚开始都对这些定义觉得莫名其妙,为什么一个 C 语言要对数据类型有这么多定义?C 语言真麻烦,我不学了!

千万不要有这种想法,如果有这种想法的同学,你一定是被 JVM 保护的像个孩子!我必须从现在开始纠正你的这个想法,因为 Java 有 JVM 的保护,很多特性都做了优化,而 C 就像个没有伞的孩子,它必须自己和这个世界打交道!

上面在说 short int 和 long int 的时候,都加了一个可能,怎么,难道 short int 和 long int 和 int 还不一样吗?

这里就是 C 语言数据类型一个独特的风格。

为什么说可能,这是由于 C 语言为了适配不同的机器来设定的语法规则,在早起的计算机上,int 类型和 short 类型都占 16 位,long 类型占 32 位,在后来的计算机中,都采用了 16 位存储 short 类型,32 位存储 int 类型和 long 类型,现在,计算机普遍使用 64 位 CPU,为了存储 64 位整数,才引入了 long long 类型。所以,一般现在个人计算机上常见的设置是 long long 占用 64 位,long 占用 32 位,short 占用 16 位,int 占用 16 位或者 32 位。

char 类型

char 类型一般用于存储字符,表示方法如下

1
2
c复制代码char a = 'x';
char b = 'y';

char 被称为字符类型,只能用单引号 ‘’ 来表示,而不能用双引号 “” 来表示,这和字符串的表示形式相反。

char 虽然表示字符,但是 char 实际上存储的是整数而不是字符,计算机一般使用 ASCII 来处理字符,标准 ASCII 码的范围是 0 - 127 ,只需 7 位二进制数表示即可。C 语言中规定 char 占用 1 字节。

其实整型和字符型是相通的,他们在内存中的存储本质是相通的,编译器发现 char ,就会自动转换为整数存储,相反的,如果给 int 类型赋值英文字符,也会转换成整数存储,如下代码

1
2
3
4
5
6
7
8
9
c复制代码#include <stdio.h>

int main(){
char a = 'x';
int b;
b = 'y';

printf("%d\n%d\n",a,b);
}

输出

120

121

所以,int 和 char 只是存储的范围不同,整型可以是 2 字节,4 字节,8 字节,而字符型只占 1 字节。

有些 C 编译器把 char 实现为有符号类型,这意味着 char 可表示的范围是 -128 ~ 127,而有些编译器把 char 实现为无符号类型,这种情况下 char 可表示的范围是 0 - 255。signed char 表示的是有符号类型,unsigned char 表示的是无符号类型。

_Bool 类型

_Bool 类型是 C99 新增的数据类型,用于表示布尔值。也就是逻辑值 true 和 false。在 C99 之前,都是用 int 中的 1 和 0 来表示。所以 _Bool 在某种程度上也是一种数据类型。表示 0 和 1 的话,用 1 bit(位)表示就够了。

float、double 和 long double

整型对于大多数软件开发项目而言就已经够用了。然而,在金融领域和数学领域还经常使用浮点数。C 语言中的浮点数有 float、double 和 long double 类型。浮点数类型能够表示包括小数在内更大范围的数。浮点数能表示小数,而且表示范围比较大。浮点数的表示类似于科学技术法。下面是一些科学记数法示例:

image-20211114091811805

C 规定 float 类型必须至少能表示 6 位有效数字,而且取值范围至少是 10^-37 ~ 10^+37。通常情况下,系统存储一个浮点数要占用 32 位。

C 提供的另一种浮点类型是 double(双精度类型)。一般来说,double 占用的是 64 位而不是 32 位。

C 提供的第三种类型是 long double ,用于满足比 double 类型更高的精度要求。不过,C 只保证了 long double 类型至少与 double 类型相同。

浮点数的声明方式和整型类似,下面是一些浮点数的声明方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码#include <stdio.h>

int main(){

float aboat = 2100.0;
double abet = 2.14e9;
long double dip = 5.32e-5;

printf("%f\n", aboat);
printf("%e\n", abet);
printf("%Lf\n", dip);

}

printf() 函数使用 %f 转换说明打印十进制计数法的 float 和 double 类型浮点数,用 %e 打印指数记数法的浮点数。打印 long double 类型要使用 %Lf 转换说明。

关于浮点数,还需要注意其上溢和下溢的问题。

上溢指的是是指由于数字过大,超过当前类型所能表示的范围,如下所示

1
c复制代码float toobig = 3.4E38 * 100.0f;printf("%e\n",toobig);

输出的内容是 inf,这表示 toobig 的结果超过了其定义的范围,C 语言就会给 toobig 赋一个表示无穷大的特定值,而且 printf 显示值为 inf 或者 infinity 。

下溢:是指由于数值太小,低于当前类型所能表示的最小的值,计算机就只好把尾数位向右移,空出第一个二进制位,但是与此同时,却损失了原来末尾有效位上面的数字,这种情况就叫做下溢。比如下面这段代码

1
c复制代码float toosmall = 0.1234e-38/10;printf("%e\n", toosmall);

复数和虚数类型

许多科学和工程计算都需要用到复数和虚数,C99 标准支持复数类型和虚数类型,C 语言中有 3 种复数类型:float _Complex、double _Complex 和 long double _Complex。

C 语言提供的 3 种虚数类型:float _Imaginary、 double _Imaginary 和 long double _Imaginary。

如果包含 complex.h 头文件的话,便可使用 complex 替换 _Complex,用 imaginary 替代 _Imaginary。

其他类型

除了上述我们介绍过的类型之外,C 语言中还有其他类型,比如数组、指针、结构和联合,虽然 C 语言没有字符串类型,但是 C 语言却能够很好的处理字符串。

常量

在很多情况下我们需要常量,在整个程序的执行过程中,其值不会发生改变,比如一天有 24 个小时,最大缓冲区的大小,滑动窗口的最大值等。这些固定的值,即称为常量,又可以叫做字面量。

常量也分为很多种,整型常量,浮点型常量,字符常量,字符串常量,下面我们分别来介绍

整数常量

整数常量可以表示为十进制、八进制或十六进制。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。

1
c复制代码330         /* 合法的 */315u        /* 合法的 */0xFeeL      /* 合法的 */048         /* 非法的:8 进制不能定义 8 */

浮点型常量

浮点型常量由整数部分、小数点、小数部分和指数部分组成。你可以使用小数形式或者指数形式来表示浮点常量。

当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

1
c复制代码3.14159       /* 合法的 */314159E-5L    /* 合法的 */510E          /* 非法的:不完整的指数 */210f          /* 非法的:没有小数或指数 */

字符常量

C 语言中的字符常量使用单引号(即撇号)括起来的一个字符。如‘a’,‘x’,’D’,‘?’,‘$’ 等都是字符常量。注意,‘a’ 和 ‘A’ 是不同的字符常量。

除了以上形式的字符常量外,C 还允许用一种特殊形式的字符常量,就是以一个 “\” 开头的字符序列。例如,前面已经遇到过的,在 printf 函数中的‘\n’,它代表一个换行符。这是一种控制字符,在屏幕上是不能显示的。

常用的以 “\” 开头的特殊字符有

image-20211109055031470

表中列出的字符称为“转义字符”,意思是将反斜杠(\)后面的字符转换成另外的意义。如 ‘\n’ 中的 “n” 不代表字母 n 而作为“换行”符。

表中最后第 2 行是用ASCII码(八进制数)表示一个字符,例如 ‘\101’ 代表 ASCII 码(十进制数)为 65 的字符 “A”。‘\012’(十进制 ASCII 码为 10)代表换行。

需要注意的是 ‘\0’ 或 ‘\000’ 代表 ASCII 码为 0 的控制字符,它用在字符串中。

字符串常量

字符串常量通常用 “” 进行表示。字符串就是一系列字符的集合。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。

常量定义

C 语言中,有两种定义常量的方式。

  1. 使用 #define 预处理器进行预处理
  2. 使用 const 关键字进行处理

下面是使用 #define 预处理器进行常量定义的代码。

1
c复制代码#include <stdio.h>#define LENGTH 5#define WIDTH 10int main(){  int area = LENGTH * WIDTH;  printf("area = %d\n", area); }

同样的,我们也可以使用 const 关键字来定义常量,如下代码所示

1
c复制代码#include <stdio.h>int main(){  const int LENGTH = 10; const int WIDTH = 5;  int area; area = LENGTH * WIDTH;  printf("area = %d\n", area); }

那么这两种常量定义方式有什么不同呢?

编译器处理方式不同

使用 #define 预处理器是在预处理阶段进行的,而 const 修饰的常量是在编译阶段进行。

类型定义和检查不同

使用 #define 不用声明数据类型,而且不用类型检查,仅仅是定义;而使用 const 需要声明具体的数据类型,在编译阶段会进行类型检查。

文章参考:
www.zhihu.com/question/19…

我自己写了四本 PDF ,非常硬核,链接如下:
cxuan 呕心沥血肝了四本 PDF。

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

本文转载自: 掘金

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

IntelliJ IDEA竟然出了可以在云端编码的功能?

发表于 2021-11-15

前言

自从我用了正版的IntelliJ IDEA后,基本上都是与时俱进,出一个新版本就立马更新,这也能能让我体验到最新最快的功能。

最近在闲逛Jetbrains的官网时,看到了最新的2021.3EAP版本的放出。立马下了试试。

而令我感到兴奋的是,2021.3版本竟然增加了云端开发的一系列功能。

所谓云端开发,就是指本地运行着一个轻客户端。所有的代码存储,代码编译,代码运行都是在远端。本地只是作一个展示和编辑作用。

这个时代很多东西都云端化了,有云电脑,云手机,云游戏。我一直都期待有个云IDE,虽然我知道vscode开源了并且有相应的云平台。但是我说IntelliJ IDEA才是Java开发IDE的天花板,这个应该没人反驳吧。

而这个EAP版本,就直接明示了以后IDEA既能作为本地IDE,又能作为轻客户端连上云端,利用云端强大的处理力,加速你的代码编译,启动速度。

而这个云端,随便指定一台服务器,就可以自动化初始化好,极其方便。

而此篇文章,就来一探究竟:这个idea云端是如何玩的。

一

安装好2021.3 EAP版本后,在欢迎界面就多出一个Remote Development选项:

点Connect via SSH,就可以看到只要通过SSH方式就可以连上一个远端服务器作为计算资源。和所有的SSH一样,填入你相应必要的验证方式,这里我是采用RSA私钥的验证方式:

点击Check Connection and Continue之后,后续所有的动作,都由IDEA自动完成。包括在你的服务器上装上远端的服务进程,初始化等事情。这个过程只能等一会。

这个过程我大概等了5分钟左右。看到成功连接上远程服务器之后,点开Project directory就可以看到服务器上的文件目录了,选择一个作为你的工作目录。

这时应该服务器上的远端进行已经开启了,我登上了服务器,查找了下idea的进程,可以发现远端服务器上启动着4个进程。

二

连上上之后进入主界面,我发现基本和本地的没有任何区别。只是在右下角多了一个即时的上行和下行的速度指标。和一个ping值。玩过手游的童鞋应该都知道,ping值是一个什么概念。ping值越低,就代表延时越低。下图可以看到,我本地和服务器的延迟是22ms,对这个值有点概念的童鞋,应该知道这个ping值基本上是感受不到延迟的。

接下来我们就感受下远端编译。

我创建了一个springboot的web项目,当然创建的代码全部在远端上面,本地只不过作为编辑界面。

运行mvn clean package命令,要注意的是此时并不是在本地执行maven命令,已经是在远端了,前提是远端服务器已经装了maven,并配置好了系统path。

整个过程和本地并无太大差别,运行结果耗时如下,这还是包括从中央仓库上下载springboot相关依赖+编译打包的时间。甚至于我都觉得比本地要来的快速。

顺便说下,这里测试时,远端服务器的配置是8核64G内存。

接下来我在demo工程上加了一个controller。直接运行这个项目。

运行很快速。配置的端口号在8989。

如果往常我们要访问controller的话,就一定会访问127.0.0.1:8989了,但是这个运行也是在远端运行的。

我查看了远端的进程和端口号,确实服务器上开启了一个线程,端口号是8989

接下来用服务器的公网地址进行访问,也是毫无问题。

甚至于debug也和本地的一模一样,毫无违和感

这基本上都可以不用在测试环境上部署了,本地直接起,相当于部署测试服务器了。用的还不是本地资源。

别的服务调用过来,本地直接可以debug了。还不用开remote debug。

当然以上纯属个人说笑,测试环境部署流程还是需要的。这里只是为了说明,这种如同本地模式的debug的确比基于配置-Xrunjdwp的remote debug好了不止一点点。

总结

整个体验下来,可以这么说,操作模式完全和本地一模一样,但是计算和存储资源却是在远端。而且我在体验的过程中,基本上没有让我感觉到卡顿的地方,如同本地般的丝滑流畅。

有些童鞋本地配置不高的,平时开几个IDEA窗口就开始卡的,完全就可以拿公司高性能的服务器作为远端。利用服务器的高性能作为计算资源。本地开个十几个轻客户端应该也不会有卡顿。不用换电脑,立马解决idea卡顿问题的神操作了。

因为这个版本只是一个EAP版本,官方文档也说了,可能在使用时中会有不稳定的异常出现。所以2021.3的正式版本,我个人还是很期待的。

可能在以后,无论是手机,还是pad,还是轻量级配置的电脑中。只要我们打开idea的轻客户端,连上强大的云服务器,无论是编译,启动,加载,都是异常的快。不会再出现,编译花个20分钟,启动花个10分钟这样的事了。

我是铂赛东,是一个开源作者和内容博主,热爱生活和分享。如果你对我的内容感兴趣,欢迎关注元人部落。

本文转载自: 掘金

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

面试官疯了吗,问我为什么浮点数不精确?

发表于 2021-11-15

很多人都知道,Java 中的浮点数并不精确,需要用 BigDecimal进行精确计算,但是,很少有人知道为什么浮点数不精确呢?不精确为什么还要用呢?本文就来展开分析一波;

我们知道,计算机的数字的存储和运算都是通过二进制进行的,对于,十进制整数转换为二进制整数采用”除2取余,逆序排列”法

具体做法是:

  • 用2整除十进制整数,可以得到一个商和余数;
  • 再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止
  • 然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。

如,我们想要把127转换成二进制,做法如下:

-w624

那么,十进制小数转换成二进制小数,又该如何计算呢?

十进制小数转换成二进制小数采用”乘2取整,顺序排列”法。

具体做法是:

  • 用2乘十进制小数,可以得到积
  • 将积的整数部分取出,再用2乘余下的小数部分,又得到一个积
  • 再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。

如尝试将0.625转成二进制:

-w624

但是0.625是一个特列,用同样的算法,请计算下0.1对应的二进制是多少:

-w624

我们发现,0.1的二进制表示中出现了无限循环的情况,也就是(0.1)10 = (0.000110011001100…)2

这种情况,计算机就没办法用二进制精确的表示0.1了。

所以,为了解决部分小数无法使用二进制精确表示的问题,于是就有了IEEE 754规范。

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。

浮点数和小数并不是完全一样的,计算机中小数的表示法,其实有定点和浮点两种。因为在位数相同的情况下,定点数的表示范围要比浮点数小。所以在计算机科学中,使用浮点数来表示实数的近似值。

IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。

其中最常用的就是32位单精度浮点数和64位双精度浮点数。

单精度浮点数在计算机存储器中占用4个字节(32 bits),利用“浮点”(浮动小数点)的方法,可以表示一个范围很大的数值。

比起单精度浮点数,双精度浮点数(double)使用 64 位(8字节) 来存储一个浮点数。

IEEE并没有解决小数无法精确表示的问题,只是提出了一种使用近似值表示小数的方式,并且引入了精度的概念。

一个浮点数a由两个数m和e来表示:a = m × b^e。

在任意一个这样的系统中,我们选择一个基数b(记数系统的基)和精度p(即使用多少位来存储)。m(即尾数)是形如±d.ddd…ddd的p位数(每一位是一个介于0到b-1之间的整数,包括0和b-1)。

如果m的第一位是非0整数,m称作规格化的。有一些描述使用一个单独的符号位(s 代表+或者-)来表示正负,这样m必须是正的。e是指数。

最后,由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,所以,千万不要在代码中使用浮点数来表示金额等重要的指标。

建议使用BigDecimal或者Long(单位为分)来表示金额。

本文转载自: 掘金

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

MySQL打印死锁日志

发表于 2021-11-15

前言:

在 MySQL 运维过程中,难免会遇到 MySQL 死锁的情况,一旦线上业务日渐复杂,各种业务操作之间往往会产生锁冲突,有些会导致死锁异常。这种死锁异常一般要在特定时间特定数据和特定业务操作才会复现,有时候处理起来毫无头绪,一般只能从死锁日志下手。本篇文章我们一起来看下 MySQL 的死锁日志。

1.手动打印死锁日志

当业务发生死锁时,首先是线上错误日志报警发现死锁异常,也会提示一些堆栈信息,然后会反馈到数据库层面进行排查。我们一般会在命令行执行 show engine innodb status\G 来输出死锁日志,\G 的作用是将查询到的结果,每行显示一个字段和字段值,方便查看。

show engine innodb status 是 MySQL 提供的一个用于查看 innodb 引擎系统信息的工具。它会输出大量的内部信息,内容分为很多小段,每一段对应 innodb 存储引擎不同部分的信息,其中 LATEST DETECTED DEADLOCK 部分显示的最近一次的死锁信息。

下面我们手动制造一次死锁,来看一下死锁日志相关信息:

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
sql复制代码------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-11-10 17:03:10 0x7fb040672700
*** (1) TRANSACTION:
TRANSACTION 46913, ACTIVE 142 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 2997198, OS thread handle 140394973071104, query id 9145673 localhost root updating
update test_tb set stu_name = 'lisi' where stu_id = 1006
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 224 page no 4 n bits 80 index uk_stu_id of table `testdb`.`test_tb` trx id 46913 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800003ee; asc ;;
1: len 4; hex 80000006; asc ;;

*** (2) TRANSACTION:
TRANSACTION 46914, ACTIVE 103 sec starting index read
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 2997201, OS thread handle 140394971473664, query id 9145681 localhost root updating
update test_tb set age = 21 where stu_id = 1005
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 224 page no 4 n bits 80 index uk_stu_id of table `testdb`.`test_tb` trx id 46914 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800003ee; asc ;;
1: len 4; hex 80000006; asc ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 224 page no 4 n bits 80 index uk_stu_id of table `testdb`.`test_tb` trx id 46914 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800003ed; asc ;;
1: len 4; hex 80000005; asc ;;

*** WE ROLL BACK TRANSACTION (2)

# 以上为原文 下面增加个人分析
------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-11-10 17:03:10 0x7fb040672700 #这里显示了最近一次发生死锁的日期和时间
*** (1) TRANSACTION: #死锁相关的第一个事务
TRANSACTION 46913, ACTIVE 142 sec starting index read
#这行表示事务id为46913,事务处于活跃状态142s,starting index read表示正在使用索引读取数据行
mysql tables in use 1, locked 1
#这行表示该事务正在使用1个表,且涉及锁的表有1个
LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
#这行表示在等待4把锁,占用内存1136字节,涉及3行记录
MySQL thread id 2997198, OS thread handle 140394973071104, query id 9145673 localhost root updating
#这行表示该事务的线程ID信息,操作系统句柄信息,连接来源、用户
update test_tb set stu_name = 'lisi' where stu_id = 1006
#这行表示事务执行的最后一条SQL信息
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: #事务1想要获取的锁
RECORD LOCKS space id 224 page no 4 n bits 80 index uk_stu_id of table `testdb`.`test_tb` trx id 46913 lock_mode X locks rec but not gap waiting
#这行信息表示等待的锁是一个record lock,空间id是224,页编号为4,大概位置在页的80位处,锁发生在表testdb.test_tb的uk_stu_id索引上,是一个X锁,但是不是gap lock,waiting表示正在等待锁
Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800003ee; asc ;;
1: len 4; hex 80000006; asc ;;

*** (2) TRANSACTION: #死锁相关的第一个事务
TRANSACTION 46914, ACTIVE 103 sec starting index read
#这行表示事务2的id为46914,事务处于活跃状态103s
mysql tables in use 1, locked 1
#正在使用1个表,涉及锁的表有1个
4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
#涉及4把锁,3行记录
MySQL thread id 2997201, OS thread handle 140394971473664, query id 9145681 localhost root updating
#事务2的线程ID信息,操作系统句柄信息,连接来源、用户
update test_tb set age = 21 where stu_id = 1005
#第二个事务的SQL
*** (2) HOLDS THE LOCK(S): # 事务2持有的锁 正是事务1想要获取的锁
RECORD LOCKS space id 224 page no 4 n bits 80 index uk_stu_id of table `testdb`.`test_tb` trx id 46914 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800003ee; asc ;;
1: len 4; hex 80000006; asc ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 224 page no 4 n bits 80 index uk_stu_id of table `testdb`.`test_tb` trx id 46914 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800003ed; asc ;;
1: len 4; hex 80000005; asc ;;
#上面这部分是事务二正在等待的锁,从信息上看,等待的是同一个表,同一个索引,同一个page上的record lock X锁,但是heap no位置不同,即不同的行上的锁

*** WE ROLL BACK TRANSACTION (2) #表示事务2被回滚

从死锁日志中可以看到关联的两个事务相关信息,当一个事务持有了其他事务需要的锁,同时又想获得其他事务持有的锁时,等待关系上就会产生循环,Innodb 不会显示所有持有和等待的锁,但死锁日志也显示了相关的信息来帮你确定,排查死锁发生的索引,这对于你确定能否避免死锁有较大的价值。

2.自动保存死锁日志

从上面内容我们知道 MySQL 的死锁可以通过 show engine innodb status 来查看,但是这个命令需要手动执行并且只能显示最新的一条死锁,该方式无法完全捕获到系统发生的死锁信息。那有没有办法记录所有的死锁日志呢,我们来看下 MySQL 的系统参数。

MySQL 系统内部提供一个 innodb_print_all_deadlocks 参数,该参数默认是关闭的,开启后可以将死锁信息自动记录到 MySQL 的错误日志中。下面我们来看下这个参数的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码# 查看参数是否开启
mysql> show variables like 'innodb_print_all_deadlocks';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_print_all_deadlocks | OFF |
+----------------------------+-------+

# 开启innodb_print_all_deadlocks,此参数是全局参数,可以动态调整。记得要加入到配置文件中
mysql> set global innodb_print_all_deadlocks = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'innodb_print_all_deadlocks';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_print_all_deadlocks | ON |
+----------------------------+-------+

建议将 innodb_print_all_deadlocks 参数设置为 1 ,这样每次发生死锁后,系统会自动将死锁信息输出到错误日志中,需要注意的是打开此参数后,只会记录死锁部分信息而不会记录 innodb 其他相关信息,即只会记录 show engine innodb status 中的 LATEST DETECTED DEADLOCK 部分。

其实 InnoDB 存储引擎还提供有 InnoDB Monitor 监视器,可以定期将 InnoDB 的状态信息输出到错误日志中,主要由 innodb_status_output 和 innodb_status_output_locks 参数控制,这两个系统变量是用来启用标准 InnoDB 监控和 InnoDB 锁监控的,开启后会将监控结果输出错误日志中,大约每隔 15 秒产生一次输出,输出内容与 show engine innodb status 一致。不过这会导致错误日志暴增,一般不建议开启这两个参数。

总结:

本篇文章介绍了 MySQL 死锁日志的获取方法,发生死锁后,可以根据死锁日志还获取相关信息。开启 innodb_print_all_deadlocks 参数可以自动将死锁信息输出到错误日志中,有助于我们及时发现并处理死锁异常。

本文转载自: 掘金

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

CodeGuide 300+文档、100+代码库,一个指导程

发表于 2021-11-15

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、路怎样走,让你们自己挑

B站 视频:www.bilibili.com/video/BV1Sr…

五年前,香河大厂村,开张大吉。我和弟兄们雄心壮志,坐公交车去面试,谁知道求职不到半个月,每天平均1.3个人挂在八股文造火箭上,一年内6个兄弟去了外包。

佛祖保佑!

算命的说我是“CRUD搬砖996”,不过我不同意。我认为出来混的,是20K是40K,要由自已决定。

你们跟着我的日子最短,底子最薄,路怎样走,让你们自已挑。

好了,祝你们,在大厂,一帆风顺! 干杯各位架构师!

二、开源仓库,带更多人上道

两年多,写了300+篇文章,100套案例代码库,开源给技术社区

点Star⭐️,收藏仓库,地址(阅读原文也可以):github.com/fuzhengwei/…
  • 仓库:github.com/fuzhengwei/… - 感谢给仓库点个 Star ⭐️
  • 描述:开源文章、代码、图稿,到 CodeGuide 让更多同好的技术伙伴参与到仓库中内容的贡献上
  • 目标:以,成体系的、全面的、有场景、带着案例的方式输出有价值的技术内容,补全市面一些资料不足的情况。创新不是把基础知识屏蔽起来进行内卷,而是像Linux开源精神一样,做到共建。
  • 内容:以,码农成长为架构师的学习成长方向为指导,编写核心编程思想和技术落地经验的学习资料。
    • 技术:Java、Spring、Mybatis、SpringBoot、Netty、ASM、Javassist、Byte-buddy、Drools
    • 开发:DDD、MVC、中间件、插件、工程框架、设计模式、开发规范、部署运维、全链路监控、技术方案
    • 项目:IM-仿微信、DDD+RPC - 分布式抽奖系统
    • 出版:《重学Java设计模式》
    • 小册:《SpringBoot中间件设计核开发》、《Netty+JavaFx实战IM》
    • PDF:《面经手册》、《手撸Spring》、《字节码编程》
    • 其他:知识星球-码农会锁
  • 使用:
    • 👉 如果你不知道自己要从哪开始、要学什么、有什么结果,请看 阅读指南 这是一个码农研发人员提升自身技术栈广度和深度的经验之路,也是小傅哥的亲身学习经历汇总!
    • 👉 如果你是刚入行、在外包、跨语言学习、想跳槽大厂、缺少学习动力等,可以阅读小傅哥的成长故事,这个系列包括了我的个人在外包到大厂的成长、跳槽的过程、互联网的学习经历 Go -> 关于小傅哥
    • 👉 推荐 在线阅读 bugstack.cn (Github 访问速度比较慢可能会导致部分图片无法刷新出来)

三、分享源码,100+套代码案例

路,不能走窄喽

100+套 的案例仓库,总能给你一些不时之需
  • 地址:github.com/fuzhengwei - 在首页进入,你会找到我所有的源码案例
  • 描述:📚 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
  • 核心:这里给大家列举一些关于小傅哥博客中的涉及到的源码库,你可以在 CodeGuide 找到,点个 Star 收藏起来就不会丢了
    1. Netty 4.x 专题 itstack-demo-netty
    2. 手写RPC框架 itstack-demo-rpc
    3. 用Java实现JVM itstack-demo-jvm
    4. 基于JavaAgent的全链路监控 itstack-demo-agent
    5. iot-gateway网关案例 itstack-demo-iot-gatewary
    6. DDD领域驱动设计落地 itstack-demo-ddd
    7. SpringCloud入门案例 itstack-demo-springcloud
    8. 微信公众号开发 itstack-ark-wx-test
    9. SpringBoot中间件开发 door-spring-boot-starter
    10. 服务框架搭建 itstack-demo-frame
    11. 源码分析(Spring、Mybatis、Schedule) itstack-demo-code
    12. Drools规则引擎 itstack-demo-drools
    13. ASM字节码编程 itstack-demo-asm
    14. 我的大学四年到毕业工作5年的学习资源和面试汇总 网盘下载,if链接失效,加微信:fustack
    15. Netty+JavaFx实战:仿桌面版微信聊天 NaiveChat
    16. JDK1.8新特性41个案例讲解 itstack-demo-jdk8
    17. 小傅哥的《字节码编程》专栏 itstack-demo-bytecode
    18. 重学Java设计模式 itstack-demo-design
    19. Java面经手册 interview
    20. Spring手撸专栏 small-spring

四、贡献力量,提交Issue、PR

慢下来,并且有意义的事,我们可以一起来做

2021年10月24日 - 程序员节👨🏻‍💻,小傅哥 把文章全部开源到代码库 CodeGuide 中,与同好同行,一起进步,共同维护。

文章越来越多、粉丝越来越多,经常需要对粉丝伙伴对文章的提问进行处理。而我一个人精力和力量毕竟有限,文章多读者多问题多,但我自己可分配的时间越来越少,维护的效率会有所下降。因此希望大家可以共同参与到 CodeGuide 的维护中。维护这样一个 6.3k star 的项目还是非常有意义,因为你的一点贡献都会被更多人看到


如何参与到项目的维护呢?

这里我提供 3 种方式:

  1. 提出 Issue :在 Issue 中指出你觉得需要改进/完善的地方(能够独立解决的话,可以在提出 Issue 后再提交 PR )。
  2. 处理 Issue : 帮忙处理一些待处理的 Issue。
  3. 提交 PR: 对于错别字/笔误这类问题可以直接提交PR,无需提交Issue 确认。

1. 提出 Issue

  1. 点击项目右上角的 New Issue 按钮,就可以创建一个新的 Issue 了。
  2. 在 Issue 中,你需要写清楚自己遇到的问题,例如:你觉得哪里需要改进,或者你觉得还可以添加哪些内容。

例如:一位同学在学习 CodeGuide 的过程中,发现面经手册中关于 HashMap 数据迁移描述性问题,于是提出了这个 Issue。

2. 处理 Issue

如果你发现一些待处理 的 Issue 的话,可以帮忙解决。

我会把一些 待处理 的 Issue 标记出来,如果你想帮忙解决这个 Issue 的话,直接在 Issue 区回复一下自己想要帮忙处理即可。

3. 提交 PR

首先,CodeGuide 仓库是属于 fuzhengwei 账户的,因此我们无权对其进行修改。所以首先,我们需要 Fork 原仓库,到自己的账户名下。

例如,点击 Fork 后,在自己账户下自动生成的仓库。

然后,我们就可以自由的对自己的仓库进行修改,编写相关内容了~因为该仓库是原仓库的复制品,所以不论你对其做什么修改,都不会影响到原仓库!

在对自己的仓库修改完毕后,你就可以向原仓库提交 Pull Request,也就是合并请求了。

那么具体该如何操作呢?

你在自己 Fork 的项目中,点击 Pull Request,然后点击 New Pull Request

然后,根据提示,创建 Pull Request 即可。是不是非常简单?

另外你还可以直接在阅读文章的时候,直接点击 在 GitHub 上编辑此页 这样也可以在已经 Fork 代码库的前提下,提交 PR,是不是很方便!

接下来,你只需要等待原仓库管理员(小傅哥)的审核就可以了。

在收到你的 Pull Request 后,原仓库管理员可以决定是否合并你的提交,如果同意合并的话,你的贡献就能真正融入原项目了!


对于提交 PR 的小伙伴来说:

  1. 解决 Issue 的过程中,你会加深自己对某个知识点的理解。
  2. 如果你的 PR 被采纳,你的贡献将会永久保留在 CodeGuide 上,你也会成为 CodeGuide 的 Contributor。

我真心希望有更多可以参与到 CodeGuide这个项目的维护中,我们大家都能有所收获!

本文转载自: 掘金

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

服务注册与发现 上手实践Spring Cloud Eur

发表于 2021-11-15

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

在微服务时代,服务的注册发现主要是为了解决两个问题,一个是屏蔽服务与服务之间依赖的细节,即解耦;另一个是满足对服务的动态管理。这篇文章就服务注册和发现,上手实现Netflix的Eureka和Feign。

一、服务注册发现模式

在实践之前,我们先来了解服务注册与发现的一些简单概念。

服务发现与注册 一般有两种实现模式:

  • 服务器端模式
  • 客户端模式

服务器端模式通过使用一个中间的服务器,来屏蔽被调用服务的复杂性与变动性,当有新的服务加入或老服务剔除时,只需要修改中间服务器上的配置即可。

这种模式,常见的做法是提供一个具备负载均衡的服务器作为中间层,比如Nginx,F5,网络传输层的Ip负载均衡,即配置集中在独立的中间服务器端完成,对代码没有任何侵入性。缺点是,调用链过程中,要透穿中间服务器,中间层势必成为一个调用链中的单点,很有可能成为性能的瓶颈。

蓝图编排设计.png

客户端服务发现模式允许服务在没有硬编码主机名和端口的情况下彼此查找和通信。这种架构中比较核心的概念是,服务必须统一注册到服务注册中心。比较有代表性的有Eureka、Consul、Zookeeper等等,其异同如下:

image.png

这种模式不需要穿透中间服务器,所以性能损耗比较小,但是需要在服务内部维护注册信息,负载均衡策略,对代码有侵入性,并且要引入一个新的注册中心服务,如果要考虑到服务的可用性以及可靠性,其维护维护成本也会增加。

image.png


少年,没看够?点击石头的主页,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

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

【ESSD技术解读-01】 云原生时代,阿里云块存储 ESS

发表于 2021-11-15

简介: 本文描述了阿里云块存储快照服务基于高性能 ESSD 云盘提升快照服务性能,提供轻量、实时的用户体验及揭秘背后的技术原理。依据行业发展及云上数据保护场景,为企业用户及备份厂商提供基于快照高级特性的数据保护的技术方案,满足云上用户数据保护的迫切需求,保障云上企业业务连续性。

2021年7月份,国际知名咨询公司 Gartner 发布了公有云的 IaaS(基础设施即服务)和 PaaS(平台即服务)平台的“魔力象限(Magic Quadrant)”,阿里云凭借其领先的技术能力首次成为“远景者”象限的公有云服务提供商,其中阿里云块存储获得单项得分第一的成绩,阿里云计算、存储,网络及安全得分获得全球第一。存储领先业界的背后离不开高性能的 ESSD 云盘产品为用户提供高可用、高可靠、高性能的块级随机访问服务及原生的快照数据保护能力。

原生业务新需求

随着云原生技术的发展,越来越多的企业基于云计算的虚拟化、弹性扩展及蓬勃发展的云原生技术的分布式框架,容器技术、编排系统、持续交付及快速迭代,构建起大规模、弹性扩展强、丰富的云上分布式业务场景。企业应用的部署规模,存储,计算等资源需求随之成指数增长,导致传统的数据保护方案无法满足云端新的技术变化。用户面临的市场竞争环境更加激烈,迫切需要适应业务规模及发展的云端数据保护方案来满足自身竞争力及业务的发展需要。虽然数据保护的业务背景及场景因云计算及云原生而发生变化,但用户对数据保护的诉求没有发生变化,衡量的标准依然是恢复时间点目标 RTO 及恢复点目标 RPO。

用户追求的首要目标依然是业务连续性,即在业务面临中断威胁,迅速实现业务恢复;业务面临增长压力,迅速实现业务扩展。用户根据业务场景对云上的数据保护及快照服务提出了如下的迫切需求:

  • 创建时间短:快照极速完成,关键业务即刻进行数据备份。
  • **极速可用:**快照极速可用,应对突发事件,完成云盘回滚恢复。
  • **业务扩展:**业务量突增需要业务扩容。
  • 整机保护:单 ECS 实例及多 ECS 实例的关联多盘的一致性数据保护。
  • **测试验证:**生产环境以外即可进行数据测试验证及恢复。
  • **恢复速度快:**文件系统及应用数据处于应用一致性的备份状态,避免应用宕机恢复过程。
  • 容器备份:容器业务环境的快速迭代及发布,迫切需要保护元数据及应用业务数据。

根据存储网络工业协会 SNIA 对快照的定义:快照是指定数据集合的一个完全可用拷贝,该拷贝包括相应数据在某个时间点(拷贝开始的时间点)的映像。阿里云块存储快照就是提供 ESSD 云盘某一时刻的一致性数据镜像。适应行业的发展趋势,快照服务不断发现用户的新需求及新场景,不懈地进行了新功能开发及迭代演进,极致升级优化 ESSD 云盘快照的高级企业新特性:快照极速可用特性、应用一致性快照及适应分布式应用架构的一致性组快照及快照跨地域复制的异地灾备功能。在不断独立输出及被集成的发展过程中,满足了云上企业用户的需求,服务大数据、游戏,人工智能、金融行业等领域,也得到了阿里云其他团队如:云数据库团队 RDS、混合云备份团队、弹性容器实例 ECI、容器服务 ACK 等业务团队及用户的反馈:

  • 云数据库团队 RDS 行业用户的评价是:RDS 的秒级备份产品对齐业界的数据库备份产品,降低原有物理文件备份对实例资源占用,有效降低了数据保护风险。
  • 弹性容器实例 ECI 容器加速收益客户图森的评价是:极速型缓存加速功能加速了容器应用发布,降低了仿真平台的计算时间,将计算任务降低到平均 5 分钟以内,产品发布周期极大缩短。
  • 按照混合云备份客户的说法,应用一致性整机备份能力完全对标 VMware 虚拟化平台的快照功能。
  • 快照服务提供的一致性组快照及应用一致性能力,完全满足 2021 年 Gartner 对阿里云块存储服务评测能力。容器业务 ACK 团队通过 2021 年 Forrestor 容器备份评测能力。

典型场景

轻量、实时的快照极速可用特性,一致性组快照及应用一致性快照的高级特性,为企业用户及第三方备份厂商快速构建起:极速备份恢复、容灾测试、副本利用及容灾切换的副本数据管理(Copy Data Management)应用场景。Gartner 于 2021 年 7 月份发布的关于存储及数据保护的技术趋势(Hype Cycle)分析中,将容器备份、云数据备份及副本数据管理(CDM)列为未来几年的数据保护的行业发展趋势。Gartner 对副本数据的管理的基本定义为:基于应用一致性的主存储快照在辅助存储上生成“Golden Image”,并利用其进行备份,容灾及测试,而且异构存储作为能力的基本条件。阿里云的 ESSD 的高级快照服务特性完全满足构建 CDM 的条件,帮助用户实现云上副本数据管理的原生数据保护典型场景:

**备份恢复:极速型备份及标准型备份相结合,提供近密远疏的备份可恢复点。**基于云上的 ECS 实例的整机保护及 K8S 环境的容器应用,定期创建极速可用快照。在启用一致性组快照特性及极速可用特性后,本地即时快照的生成间隔可以到秒级。快照即时副本本地保留,成为极速型备份,用于秒级 IO 性能无损恢复。周期性基于上层的企业应用生成整机应用一致性快照。本地快照副本同时通过网络上传到对象存储 OSS 上作为标准型备份。标准型备份在完成备份数据上传后,本地域全可用区可见,适合保留时间长的历史数据。

**容灾测试:基于极速型备份的容灾测试。**副本数据管理中要求对灾备环境定期测试。定期的测试可以提高灾备环境的可靠性,避免配置问题和环境变更问题使得真的灾难发生时,容灾切换无法正确完成,从而导致业务无法快速进行容灾系统恢复。基于本地快照副本的极速克隆技术,灾备实例及拉起容器应用,周期性进行挂载及备份数据测试验证。传统基于复制技术的方案,需要等待快照在灾备端复制可用后才能进行测试演练。而采取极速型备份方式后,实现灾备端的秒级克隆,秒级挂载及秒级启动测试。

**副本利用:基于极速型备份的数据分析。**在不影响生产环境的情况下,灾备环境下基于极速克隆技术,进行容器应用的定时拉起,对副本进行大数据计算及分析,挖掘数据价值。副本利用在实践中也体现在 MySQL 数据库应用基于极速型备份进行只读备库的即时拉起,进行离线数据分析。

**容灾切换:业务从生产环境切到灾备环境。**当生产发生较大灾难时,短时间无法恢复业务,生产无法继续,将业务从生成中心切换到灾备中心;在生产中心业务恢复后,再将业务进行容灾切回。

相比于传统的副本数据管理 CDM 方案,云计算环境及云原生环境拥有大规模弹性的同构的计算环境,企业用户不必进行设备资源及软件投入;极速型备份及极速型克隆技术极大地降低了副本开发、测试及容灾切换的恢复时间点目标 RTO;云上快照服务的统一的备份数据格式降低了各种管理流程中所需的副本数量,消除了备份软件之间数据格式兼容性问题。

技术原理

我们对分布式快照算法和实现进行了大量优化,让用户可以抛开影响性能的顾虑,随时进行轻量、实时的数据保护。“轻”: 在快照创建期间不影响 IO 读写性能。“快”:ESSD 云盘快照可以在秒级创建、秒级回滚和秒级克隆-极速可用特性,满足用户实时数据保护和 DevOps 快速编排上的需要。

极速可用特性

具有极速可用特性的快照服务,不仅能够进行数据备份、合规场景及长期归档业务,而且云盘数据可以一键备份到阿里云的对象存储服务(Object Storage Service)上,与秒级间隔的本地快照副本保留形成近密远疏的快照保护策略,实现快照轻量创建,实时可用的极速克隆,秒级无损回滚的高级特性。

**极速克隆:**在隔离于生产的跨可用区的容灾环境,快照克隆新盘实现可写快照,应用测试验证及业务恢复准备;消除云上业务压力,实现业务横向扩容。比如 MySQL 数据库应用的横向扩容、备库搭建,实例创建及读写分离的都需要秒级拉起,极速克隆通过延迟加载技术实现本地快照副本的本地域内及跨集群的秒级数据可用,迅速克隆新盘,实现实例秒级拉起。

**秒级回滚:**本地快照副本数据与云盘本地存储,实现秒级 IO 无损回滚恢复。快照生成过程基于改进型的 ROW 技术及全息索引技术,随着写入 ESSD 的云盘数据块变化,依据 ESSD 云盘 IO 性能读取的最佳模式进行云盘读取性能的优化。无需从远端对象存储上拉取数据,达到秒级回滚 IO 性能无损。

在云盘创建多个极速可用快照后及发起回滚后的测试条件下,云盘性能读取性能基本无变化。某友商的云盘在保留多个本地快照后,IO 读取性能出现不同程度的延迟抖动。

一致性组快照

容器环境及 ECS 实例需要保护关联多盘的有状态应用。单盘快照的最大问题是:有状态应用基于跨多云盘LVM、Windows 动态盘及文件系统作为持久化存储,单云盘快照数据备份错误;数据库应用既兼顾性能又兼顾数据安全性,将日志文件 WAL 与数据文件分别位于不用的存储设备,无法定期进行系统整机备份及容灾。

除了 K8S下的 POD 内有状态应用的部署及单 ECS 实例部署方式外,云环境下还存在着分布式应用的部署架构、应用高可用集群如:Windows Failover Cluster、主备应用服务器高可用架构、Oracle RAC 基于共享存储的应用架构,而这些分布式架构同样需要跨云盘及跨节点的数据一致性保护要求。

云计算存储后端往往采用分布式存储架构。在分布式环境下缺少全局逻辑时钟,这就使得实现单 ECS 实例及跨 ECS 实例,K8S 环境下的单 POD 及跨节点的多云盘的一致性组快照不是件容易的事情。要实现快照对 IO 性能影响最低更是富有技术挑战性的。业界针对多盘崩溃一致性快照的实现技术主要分为两大类:

  • 采取快照期间阻塞写 IO 的方式,实现基于时间点的跨多盘数据崩溃一致性
  • 采取逻辑时钟的定序算法,但依赖于分布式存储实现,实现难度较高。

一致性组快照采取第二种方式,追求快照对 IO 性能无损,实现快照对应用性能影响到最小

**实现原理:**采取基于 IO 定序算法,快照创建无需写 IO 阻塞。很多用户担心创建快照影响 IO 性能,只在业务低谷期才进行快照数据保护。我们优化提升的多盘一致性组快照算法打破了人们对快照 IO 影响印象,基于写顺序保序机制,主动按照写 IO 到达底层存储的顺序,采取 IO 打标及定序过程。基于快照完成时刻点及 IO 定序来确定快照中应该包含的 IO 数据集合。由于快照定序过程相对于传统的方式,不会阻止 IO 写入过程;相比于传统的写时拷贝 COW 方式,快照生成过程采取写时重定向 ROW 的写入方式,后台数据集合引用生成过程对 IO 链路无影响,降低快照对 IO 性能的影响最小,对数据库业务的读写场景实现了 IO 性能无损。

对数据库应用使用 2 块盘, 2 个客户端,容量为 4TB,随机写,iodepth=16,jobs=1, 写入块大小 16KB 的测试数据库高 IOPS 场景中,快照创建过程中对 IO 影响测试,友商1及友商2的快照创建过程中对 IO 的性能影响几乎增加了 1 到 3 倍。

应用一致性快照

ESSD 云盘快照数据的一致性类型主要分为崩溃一致性和应用一致性。崩溃一致性要求文件系统及应用程序具有宕机恢复能力,其特点是恢复点目标 RPO 低,业务影响小。但在以下场景无法满足数据备份可靠性高及秒级恢复时间点目标 RTO:

  • **原子性缺陷风险:**文件系统及数据库应用实现事务原子性的实现具有一定的难度,可能存在缺陷。系统顶级会议 USENIX 上发表的《All File Systems Are Not Created Equal》一文阐释了应用程序及内核保证原子性可能存在实现缺陷。
  • 数据丢失风险:主流文件系统默认以性能优先方式工作,崩溃一致性备份存在数据丢失风险。 Linux 上 ext4 文件系统默认数据写入模式为 ordered 模式,文件系统校验修复过程存在数据丢失风险;数据库应用配置为性能优先,业务数据有丢失风险。
  • **生成时间长及影响大:**传统文件级物理备份方式及备份代理方式依赖于逻辑卷快照的生成,耗时长及系统影响大。备份代理需要安装内核驱动,兼容性差及维护成本高;文件备份过程需要读取数据,耗费系统 CPU 及 IO 资源。应用一致性快照仅在生成一致性时间点与应用互通,无增量数据生成及备份读写操作。

**实现原理:**与传统备份方式相比,应用一致性快照对用户的价值在于提供云原生的无代理应用一致性快照,简化了客户使用传统备份方式所产生的:资源消耗,发布复杂性、软件兼容性,内核开发,软件维护的成本。采取跨平台插件与专有一致性组件相结合的方式,基于文件系统内核及 Windows 上的 VSS 机制实现快照期间 IO 及应用事务的数据静默,达到企业应用程序在存储快照中的数据一致性要求。所采取的生成协议基于影响时长自动恢复 IO 影响,快照一致性类型取决于创建协议提交结果及应用状态,优化从上层应用到底层存储的链路长度及一致性组件性能,将 IO 影响时长降低到秒级。创建频率间隔可根据业务要求做到文件系统一致性秒级完成创建及分钟级应用一致性快照间隔。

从崩溃一致性到应用一致性,从单盘一致性快照到多云盘组快照的一致性,ESSD 快照的一致性分类实现完全对标业界块存储公有云全类型的快照一致性分类。从安全风险及应用支持可扩展性上与友商实现对比,实现的原生无代理快照的优势:无常驻服务,无公网 IP 地址及端口开放风险,角色安全授权,无额外内核驱动参与;支持动态发现逻辑卷及企业应用。基于 ESSD 云盘存储快照,无代理备份,无需维护内核驱动,虚拟机内部无数据读取搬运。

通过实际对国内外主要云厂商的快照创建时长及 IO 影响时长测试,基于 ESSD 系统盘及数据盘的 SQL Server 数据库应用能够实现秒级写 IO 阻塞及分钟级快照间隔,应用一致性快照的创建时长比友商降低了 2 到 3 倍。应用一致性的整机恢复,避免崩溃一致性快照恢复时日志重放过程,从而提高了数据库应用的启动速度。

业界功能对比

与业界公有云其它友商的快照特性横向对比,ESSD 云盘是目前唯一个全面支持快照极速可用特性及一致性组快照的云厂商,满足企业核心应用上云的数据保护场景对快照 RTO 及 RPO 的要求。

未来展望

**数据保护不是亡羊补牢而应未雨绸缪。**随着云原生技术的蓬勃发展,特别是容器技术的演进,企业用户对云上保护的恢复点目标 RPO 及恢复时间点目标 RTO 的要求越来越高。后续,我们也将基于 ESSD 云盘推出更多新功能,比如:高密快照、连续数据保护,基于多 ECS 实例的应用一致性保护能力,继续为用户提供快照特性的“轻”、“快”及“弹”的特性品质,降低企业数据保护的 RTO 及 RPO,提供更多原生快照服务高级特性,助力企业数据保护。

原创作品:阿里云存储 凡钧

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

SpringAop源码-基于AspectJ注解的方式

发表于 2021-11-15

官方aop

1.SpringAOP的基本概念

Aspect切面

就是定义切入点、通知等载体.如果使用@AspectJ注解的方式它就是一个类,如果使用Schema-based(springxml)的方式,它就是一个标签.

例如以@AspectJ注解的方式

1
2
3
4
5
6
java复制代码@Aspect
public class PointCutExample {
// 定义一个PointCut切点
@Pointcut("execution(* com.spring.Aop..service.*.*(..))")
public void businessService() {}
}
1
2
3
4
5
6
7
java复制代码@Aspect
public class AdviceExample {
//定义一个advice通知
@Before("com.spring.Aop.aspect.PointCutExample.businessService()")
public void before(JoinPoint joinPoint) {
log.info("准备执行方法:{},参数列表:{}",joinPoint, Arrays.asList(joinPoint.getArgs()));
}

也可以把通知和切点都写到一个Aspect切面里.
以Schema-based xml的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码 <!--目标对象一个普通bean-->
<bean id="userServiceImpl" class="com.spring.Aop.service.。在上例中目标点就是UserServiceImpl类中的"></bean>

<!--声明一个advice-->
<bean id="logArgsAdvice" class="com.spring.Aop.aspect.LogArgsAdvice"></bean>

<aop:config>
<!--定义切点-->
<aop:pointcut id="logArgsPointCut" expression="execution(* com.spring.Aop..service.*.*(..))"></aop:pointcut>
<!--定义advice通知-->
<aop:aspect ref="logArgsAdvice">
<aop:before method="before" pointcut-ref="logArgsPointCut"></aop:before>
</aop:aspect>
</aop:config>
1
2
3
4
5
6
7
java复制代码//advice通知并没有加注解@AspectJ
public class LogArgsAdvice {

public void before(JoinPoint joinPoint) throws Throwable {
log.info("准备执行方法:{},参数列表:{}",joinPoint, Arrays.asList(joinPoint.getArgs()));
}
}

JoinPoint连接点

叫目标点更好理解.就是需要进行拦截的目标方法,在Aop的理念中很多地方都可以作为连接点,进行添加横切逻辑.但springAop中连接点就单指某个方法。在上例中连接目标点就是UserServiceImpl类中的所有方法。

PointCut切点

定义查找匹配连接点的表达式,可以简单的认为PointCut就是JoinPoint的集合.
代表被表达式匹配上的所有连接点.

advice通知

横切逻辑,前置通知、后置通知等等

注意:

@AspectJ注解使用了AOP的概念.跟AspectJ是没有关系的,只是用了个名字。

2.代码实现基于@AspectJ注解方式

定义连接点JoinPoint,也就是需要代理的目标方法 UserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typescript复制代码//UserService接口
public interface UserService {
User createUser(String name,Integer age);
User queryUser();
}
//UserServiceImpl实现类
@Slf4j
public class UserServiceImpl implements UserService {
private static User user = null;

@Override
public User createUser(String name, Integer age) {
user = new User();
user.setName(name);
user.setAge(age);
log.info("UserServiceImpl createUser 方法执行中....");
return user;
}

@Override
public User queryUser() {
log.info("UserServiceImpl queryUser 方法执行中....");
return user;
}
}

定义pointCut切点,就是匹配查找JoinPoint连接点的表达式 UserPointCut.java

1
2
3
4
5
6
7
java复制代码// 定义PointCut
@Aspect
public class UserPointCut {

@Pointcut("execution(* com.spring.Aop..service.*.*(..))")
public void businessService() {}
}

定义Advice通知,也就是需要增强的横切逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Slf4j
@Aspect
public class UserAdvice {

@Before("com.spring.Aop.aspect.UserPointCut.businessService()")
public void before(JoinPoint joinPoint) {
log.info("准备执行方法:{},参数列表:{}",joinPoint, Arrays.asList(joinPoint.getArgs()));
}

@AfterReturning(pointcut = "com.spring.Aop.aspect.UserPointCut.businessService()",returning="returnValue")
public void afterReturning(JoinPoint joinPoint,Object returnValue){
log.info("{}方法执行返回:{}",joinPoint,returnValue);
}

application xml配置文件 springAop-2.0aspectJ.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<!--普通bean-->
<bean id="userService" class="com.spring.Aop.service.UserServiceImpl"></bean>

<!--定义advice通知-->
<bean id="userAdvice" class="com.spring.Aop.aspect.UserAdvice"></bean>

<!--开启@AspectJ配置-->
<aop:aspectj-autoproxy/>

</beans>

测试代码

1
2
3
4
ini复制代码ClassPathXmlApplicationContext applicationContext =  new ClassPathXmlApplicationContext("classpath:springAop-2.0aspectJ.xml");
UserService userService = applicationContext.getBean(UserService.class);
userService.createUser("Jom",12);
userService.queryUser();

运行结果:

image.png

3.SpringAop的底层原理

代理模式是springAop的实现原理,但是具体是怎么的呢?如果让我们自己是实现springAop的功能,会怎么做?

3.1动态代理的原理

①.调用Proxy.newProxyInstance生成代理对象.

②.继承InvocationHandler接口,实现Invoke方法,可以在invoke方法添加各种通知横切逻辑,同时调用目标对象的方法.

③.JDK自动生成的代理对象会继承目标对象的接口,并实现目标对象需要代理的方法.方法里再调用②中的invoke方法,而invoke方法中又调用了目标对象的方法,这样就实现了代理的功能.

3.2自己实现aop的思路

①.首先要一个处理aop的工具类

②.找到需要代理的目标类

③.工具类里实现Invoke方法,将寻找到的advice和目标方法调用添加进去.

④.生成代理类对象.因为依赖Jdk动态代理,实现invoke方法将目标对象传入进去即可.

问题:

1.IOC容器如何区分普通类和代理类?— 通过接口BeanPostProcessor区分的

2.因PointCut切点是匹配表达式,就有可能匹配到很多不同的service方法,会生成一个代理对象包含所有的方法代理?还是生成多个代理对象? —对于单个被代理的目标对象来说只会生成任意一个代理对象,但在实际执行invoke里会循环执行全部匹配上的advice执行链.

4.源码实现

4.1.查找到处理aop的工具类

在这里是AnnotationAwareAspectJAutoProxyCreator,那怎么找到该类呢?

1.找到开头标签aop对应的命名空间
image.png

2.全局搜索命名空间:www.springframework.org/schema/aop 注意http后需加一个 “\”,在spring.handlers找到对应的Handler
image.png
3.再根据Handler找到对应的Parser,这里是AspectJAutoProxyBeanDefinitionParser
image.png
4.AspectJAutoProxyBeanDefinitionParser代码如下,parse()方法里会注册AnnotationAwareAspectJAutoProxyCreator类.

1
2
3
4
5
6
7
8
9
less复制代码//解析方法
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
//注册类到Ioc容器
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
extendBeanDefinition(element, parserContext);
return null;
}

进入registerAspectJAnnotationAutoProxyCreatorIfNecessary方法

1
2
3
4
5
6
7
8
9
10
11
scss复制代码public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
ParserContext parserContext, Element sourceElement) {
//将一个类定义注册到IOC容器,并返回对象BeanDefinition
//这个就是AnnotationAwareAspectJAutoProxyCreator.class
BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
parserContext.getRegistry(), parserContext.extractSource(sourceElement));
//处理proxy-target-class与expose-proxy属性,将beanDefinition对应的属性设置为true/false
useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
//注册组件
registerComponentIfNecessary(beanDefinition, parserContext);
}

image.png
最终注册bean的key就是这个,下面会进行debug验证.

4.2 Aop处理类Creator何时被加载

那么springIOC容器是知道加载这个类呢?在何时加载的呢?

此处涉及到IOC知识,不详细展开。spring在读取xml配置文件时,会解析xml中的配置,也包括xml文件中的声明部分.
先看下IOC的启动流程,AbstractApplicationContext的refresh()方法.

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
scss复制代码@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// Prepare this context for refreshing.
prepareRefresh();

// a.此处加载AnnotationAwareAspectJAutoProxyCreator的bean定义
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// b.注册和创建AnnotationAwareAspectJAutoProxyCreator对象
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// c.注册创建普通的bean对象
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

先看a处的代码,进入到obtainFreshBeanFactory()方法,发现里面又调用了refreshBeanFactory()方法.
refreshBeanFactory在AbstractRefreshableApplicationContext中代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
//对IOC容器进行定制化,如设置是否允许循环引用、是否允许bean定义覆盖重写等
customizeBeanFactory(beanFactory);
//加载Bean定义
loadBeanDefinitions(beanFactory);
this.beanFactory = beanFactory;
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

loadBeanDefinitions方法最终会调用执行上面的AspectJAutoProxyBeanDefinitionParser的parse方法.loadBeanDefinitions执行后debug如下

image.png
可以看到BeanDefinitionMap里除了我们自己定义的bean之外,还多了一个org.springframework.aop.config.internalAutoProxyCreator。

4.2.创建Aop处理类Creator的对象

先看看Creator类的继承关系图如下
image.png
可以看到该类继承实现了BeanPostProcessor接口.这个接口大家应该很熟悉,就是spring设计的扩展功能实现.

另外关于动态代理的执行时机问题,是需要目标对象被创建初始化后,才能进行动态处理操作.不然目标对象都没有,代理谁呢?不符合逻辑.

从IOC的启动流程来说,有了Bean定义同时又实现了BeanPostProcessor接口.故在registerBeanPostProcessors(beanFactory)时会将该Creator类注册到容器里,参看4.2 b处的代码.注意这里注册就会创建Creator类对象了,创建的时机在普通的bean之前,这点尤其重要.

registerBeanPostProcessors源码如下:

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
scss复制代码public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
//查找实现了BeanPostProcessor的类
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

// Separate between BeanPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
// 存放实现PriorityOrdered的PostProcessors
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
// 存放实现MergedBeanDefinitionPostProcessor的PostProcessors
List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
// 存放实现Ordered的PostProcessors
List<String> orderedPostProcessorNames = new ArrayList<>();
// 存放以外情况的PostProcessors
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
// 此时PostProcessors对象还没有被创建,所以存放beanName
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// First, register the BeanPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

// Next, register the BeanPostProcessors that implement Ordered.
// 这里注册实现了Ordered的BeanPostProcessors
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String ppName : orderedPostProcessorNames) {
// 重点:创建BeanPostProcessors对象 最终会调用执行doCreateBean方法.
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
//将创建好的BeanPostProcessors对象 存放到beanPostProcessors列表中.
registerBeanPostProcessors(beanFactory, orderedPostProcessors);

// Now, register all regular BeanPostProcessors.
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

// Finally, re-register all internal BeanPostProcessors.
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);

// Re-register post-processor for detecting inner beans as ApplicationListeners,
// moving it to the end of the processor chain (for picking up proxies etc).
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}

到这里AnnotationAwareAspectJAutoProxyCreator类的对象已经创建好了,提前创建此种类型对象spring是通过其继承实现了BeanPostProcessor识别的.

4.4 创建普通Bean对象,如何变成代理对象的

创建普通Bean对象也就是需要代理的目标对象,在refresh()方法的finishBeanFactoryInitialization(beanFactory)里面,参看4.2的c处标记。
通过该方法一直点下去,最终会调用到AbstractAutowireCapableBeanFactory.doCreateBean()真正创建对象的地方.如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码//省略。。。。
if (instanceWrapper == null) {
// 创建对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//省略。。。。
// Initialize the bean instance.
Object exposedObject = bean;
try {
// 填充Bean,设置属性啥的
populateBean(beanName, mbd, instanceWrapper);
// 初始化Bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
//省略。。。。

进入initializeBean方法

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
ini复制代码protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
// 1. 执行每一个 BeanPostProcessor 的 postProcessBeforeInitialization 方法
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
// 调用 bean 配置中的 init-method="xxx"
invokeInitMethods(beanName, wrappedBean, mbd);
}
....
if (mbd == null || !mbd.isSynthetic()) {
// 2.重点关注 执行每一个 BeanPostProcessor 的 postProcessAfterInitialization方法
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

applyBeanPostProcessorsAfterInitialization会循环调用IOC容器中所有的BeanPostProcessor,上面创建的Creator对象的postProcessAfterInitialization在这里会被执行,执行后目标对象就被包装成代理对象了。
至此IOC是如何加载代理对象和普通目标对象就清晰了。接下来就是SpringAop主要内容了。

4.5 创建代理对象

被调用的postProcessAfterInitialization方法在AnnotationAwareAspectJAutoProxyCreator的父接口AbstractAutoProxyCreator中,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
//此处的参数bean和beanName就是目标对象,此时目标对象已经被创建出来了.
if (bean != null) {
//从缓存中获取bean的名字key,如果是FactoryBean类型的key前面加&标识,以外的正常返回
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
//执行处理,处理完如果是aop类型就返回代理后的对象,给到ioc容器
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

wrapIfNecessary方法的详细处理

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
kotlin复制代码protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
//beanName不可空 && 已经处理过的直接返回
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
//advisedBeans中包含是类型为Advice、Pointcut、Advisor、AopInfrastructureBean都跳过直接返回.
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
//同样的处理
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// 4.5.1 寻找匹配该目标Bean的横切逻辑方法(advice和advisor之类的)
// 在我们上面的例子就是UserAdvice里的before和afterReturning方法
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
//4.5.2生成真正的代理对象proxy,即利用JDK动态代理或cglib生成的$proxy0@xxx对象
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

这步处理结束后、就返回想要的代理对象了.

4.5.1 寻找匹配目标Bean的通知方法(advice和advisor)

getAdvicesAndAdvisorsForBean是抽象方法,具体的实现有2个子类分别是AbstractAdvisorAutoProxyCreator和BeanNameAutoProxyCreator。
而AbstractAdvisorAutoProxyCreator是上面注册的AnnotationAwareAspectJAutoProxyCreator的父类,故找AbstractAdvisorAutoProxyCreator中相应的方法.

1
2
3
less复制代码@Nullable
protected abstract Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName,
@Nullable TargetSource customTargetSource) throws BeansException;

image.png
AbstractAdvisorAutoProxyCreator中getAdvicesAndAdvisorsForBean()

1
2
3
4
5
6
7
8
9
less复制代码protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
//查找合适的advisor(继承和注解advisor的类)
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}
1
2
3
4
5
6
7
8
9
10
11
scss复制代码protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
//先找到容器中全部的advisor
List<Advisor> candidateAdvisors = findCandidateAdvisors();
//再匹配beanName对应的advisor
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}
4.5.1.1 查找全部的advisor

findCandidateAdvisors()方法,上面的处理是在AbstractAdvisorAutoProxyCreator中,而findCandidateAdvisors方法在其类中有实现,且在子类中也有实现,此处应该去AnnotationAwareAspectJAutoProxyCreator中的实现,优先调用自己的方法

1
2
3
4
5
6
7
8
9
10
kotlin复制代码protected List<Advisor> findCandidateAdvisors() {
// 找到所有继承了Advisor.class的子类
List<Advisor> advisors = super.findCandidateAdvisors();
// Build Advisors for all AspectJ aspects in the bean factory.
if (this.aspectJAdvisorsBuilder != null) {
//寻找注解的advisor
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}

经过处理后的advisors里面封装了pointCut和advice信息

4.5.1.2 匹配目标beanName对应的advisor

findAdvisorsThatCanApply()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
if (candidateAdvisors.isEmpty()) {
return candidateAdvisors;
}
List<Advisor> eligibleAdvisors = new ArrayList<>();
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
eligibleAdvisors.add(candidate);
}
}
boolean hasIntroductions = !eligibleAdvisors.isEmpty();
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor) {
// already processed
continue;
}
if (canApply(candidate, clazz, hasIntroductions)) {
eligibleAdvisors.add(candidate);
}
}
return eligibleAdvisors;
}

4.5.2 生成具体的代理对象

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy

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
scss复制代码protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
// 判断proxy-target-class="true"
if (proxyFactory.isProxyTargetClass()) {
if (Proxy.isProxyClass(beanClass)) {
for (Class<?> ifc : beanClass.getInterfaces()) {
proxyFactory.addInterface(ifc);
}
}
}
else {
// No proxyTargetClass flag enforced, let's apply our default checks...
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

// Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) {
classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
}
return proxyFactory.getProxy(classLoader);
}

可以看到proxyFactory对象封装了advisors和目标对象的信息.
proxyFactory.getProxy()

1
2
3
less复制代码public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}
1
2
3
4
5
6
kotlin复制代码protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}

根据配置先创建AopProxy对象,再根据AopProxy创建具体的proxy对象.是用JDK动态代理还是Cglib方式.
DefaultAopProxyFactory类createAopProxy()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
//此处的config参数就是proxyFactory对象
if (!NativeDetector.inNativeImage() &&
(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
Class<?> targetClass = config.getTargetClass();
...
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

我们只看JdkDynamicAopProxy方法在JdkDynamicAopProxy类中

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
//如果不存在Advisor或者目标对象为空的话 抛出异常
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
throw new AopConfigException("No advisors and no TargetSource specified");
}
this.advised = config;
//获取advised里代理对象所继承的所有接口
this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
//判断标记继承的接口是否有实现equals和hashCode方法
findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces);
}

创建具体的代理对象

1
2
3
4
5
6
7
less复制代码@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
}

至此代理对象就创建完成了.

4.6 执行调用Aop对象

此处只看JdkDynamicAopProxy JDK动态代理的方式
JDK动态代理的原理,生成的代理对象的方法内部会委托给InvocationHandler.invoke()方法。而invoke方法就会包含各个增强方法(通知),再调用原对象的方法。
看一下JdkDynamicAopProxy的源码实现了InvocationHandler接口

1
kotlin复制代码final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

重点invoke()

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
kotlin复制代码public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
//method.getDeclaringClass获取Method对应的目标类,method.getClass获得是Method类
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
//Advised接口或者其父接口中定义的方法,直接反射调用,不应用通知
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}

Object retVal;

if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
//获取目标对象的类
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// 获取目标方法的拦截器链.程序上我们可以定义多个Aop切面,而某个目标方法就有可能被多个Aop匹配上,故需要循环执行每个匹配的aop通知.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
//拦截器链为空的,则直接调用目标对象方法
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 创建MethodInvocation
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}

//最终方法执行的返回结果
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}

ReflectiveMethodInvocation的proceed()

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
kotlin复制代码public Object proceed() throws Throwable {
// 如果执行Interceptor完了,则执行joinPoint.注意currentInterceptorIndex初始值为-1
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
// 执行Interceptor,根据currentInterceptorIndex逐个获取Interceptor
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
//是否需要动态匹配
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
//执行当前IntercetporAdvice的invoke方法
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

为什么@Before、@After的Advice的invoke可以实现前置、后置的通知效果?
MethodBeforeAdviceInterceptor的invoke()

1
2
3
4
5
scss复制代码public Object invoke(MethodInvocation mi) throws Throwable {
//先执行before方法,执行完后再回调ReflectiveMethodInvocation.proceed()
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}

AfterReturningAdviceInterceptor的invoke()

1
2
3
4
5
6
7
java复制代码public Object invoke(MethodInvocation mi) throws Throwable {
//先回调ReflectiveMethodInvocation.proceed(),因为调用joinPoint在这里
Object retVal = mi.proceed();
//再执行afterReturning
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}

结束。

5.总结

Aop首先需要理解基础的概念,有哪些具体是什么用处,怎么使用的。

其次理解动态代理的原理,这是理解aop的基础。

最后具体的aop实现步骤,先创建代理对象 -> 创建目标对象时 -> 判断是否有匹配上的代理对象 ->找到所有的advice和pointCut -> 再找到和目标对象匹配的advice和pointCut -> 生成实际的代理对象 -> 程序调用 -> 代理对象里会调用invoke方法 -> 执行具体的横切逻辑通知 -> 执行调用目标对象.

另仅个人学习记录,有理解不到位的情况…

本文转载自: 掘金

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

leetcode 1286 Iterator for Co

发表于 2021-11-15

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

描述

Design the CombinationIterator class:

  • CombinationIterator(string characters, int combinationLength) Initializes the object with a string characters of sorted distinct lowercase English letters and a number combinationLength as arguments.
  • next() Returns the next combination of length combinationLength in lexicographical order.
  • hasNext() Returns true if and only if there exists a next combination.

Example 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码Input
["CombinationIterator", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[["abc", 2], [], [], [], [], [], []]
Output
[null, "ab", true, "ac", true, "bc", false]

Explanation
CombinationIterator itr = new CombinationIterator("abc", 2);
itr.next(); // return "ab"
itr.hasNext(); // return True
itr.next(); // return "ac"
itr.hasNext(); // return True
itr.next(); // return "bc"
itr.hasNext(); // return False

Note:

1
2
3
4
vbnet复制代码1 <= combinationLength <= characters.length <= 15
All the characters of characters are unique.
At most 10^4 calls will be made to next and hasNext.
It's guaranteed that all calls of the function next are valid.

解析

根据题意,设计一个 CombinationIterator 类,里面需要包涵几个函数:

  • CombinationIterator(string characters, int combineLength) 使用已经经过排序的不同小写英文字母的字符串 characters 和数字 combinationLength 作为参数初始化对象
  • next() 按字典顺序返回长度为 combinationLength 的下一个组合
  • hasNext() 当且仅当存在下一个组合时才返回 true

这道题的关键在于找出所有的组合,至于 next 和 hasNext 两个函数只需要做简单的逻辑判断即可,第一种方法使用了内置函数 itertools.combinations 来找出所有长度为 combinationLength 的组合,简单粗暴。

解答

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
python复制代码class CombinationIterator(object):

def __init__(self, characters, combinationLength):
"""
:type characters: str
:type combinationLength: int
"""
self.characters = characters
self.L = [''.join(i) for i in itertools.combinations(characters, combinationLength)]


def next(self):
"""
:rtype: str
"""
return self.L.pop(0)


def hasNext(self):
"""
:rtype: bool
"""
if self.L:
return True
return False

运行结果

1
2
rust复制代码Runtime: 44 ms, faster than 81.82% of Python online submissions for Iterator for Combination.
Memory Usage: 16 MB, less than 72.73% of Python online submissions for Iterator for Combination.

解析

当然了使用内置函数显得没有水平,我们还可以自己写代码,自定义函数 permute 使用 DFS 找出所有的组合。

解答

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
python复制代码class CombinationIterator(object):

def __init__(self, characters, combinationLength):
"""
:type characters: str
:type combinationLength: int
"""
self.characters = characters
self.n = combinationLength
self.N = len(characters)
self.L = []
self.permute('', 0)

def permute(self, s, start):
if len(s) == self.n:
self.L.append(s)
return
else:
for i in range(start, self.N):
self.permute(s + self.characters[i], i + 1)

def next(self):
"""
:rtype: str
"""
return self.L.pop(0)


def hasNext(self):
"""
:rtype: bool
"""
if self.L:
return True
return False

运行结果

1
2
rust复制代码Runtime: 52 ms, faster than 54.55% of Python online submissions for Iterator for Combination.
Memory Usage: 15.9 MB, less than 72.73% of Python online submissions for Iterator for Combination.

原题链接:leetcode.com/problems/it…

您的支持是我最大的动力

本文转载自: 掘金

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

1…339340341…956

开发者博客

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