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

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


  • 首页

  • 归档

  • 搜索

【译】想成为一个出色的Web工程师?学Golang而非Nod

发表于 2019-10-25

原文:medium.com/codezillas/…

Nodejs是一个运行时或者说是一个Javascript代码运行时的环境,而Golang是一门编程语言但不仅仅用于后端开发。Golang是由Google于2007年发明的。Nodejs能让Javascript代码运行在服务端上,而Golang适用于服务器端编写代码。Nodejs代码也就是Javascript的代码,所以继承了大量的Javascript特性。Javascript在当下是如此的流行,以至于ECMAScript标准主要是改进性地前行而不是颠覆性地进行。Golang可以被认为是从c/c++这些更保守的语言上的进化。Golang核心结构的建立是在C和Python的基本准则之上的。

我们接下来会对比一下Golang与Nodejs的优缺点,从而让你更好地选择在接下来的工作里面该选谁。

性能

  • Golang:与c/c++差不多的性能特性,可以说是非常出色了。
  • Nodejs:如果从原始性能上来说,Golang和Nodejs比较是相对公平的。
    原始性能即CPU没有额外的负荷,Golang和Nodejs在这方面都相对展示相对令人满意的效果,但是Golang在实际运行性能可能会更好,因为服务端运行时候的性能都各种因素的影响。

并发

  • Golang:Golang使用一种并发模型叫“Goruntinues”。Goruntinues允许多个协程同时运行,轻松靠谱地完成并发任务。
  • Nodejs:Nodejs是单线程的,意味着执行任务是顺序执行的。在大规模扩展的同时需要处理多个并行任务在同一时间可能会出现一些致命的弱点。Javascript允许利用事件回调的方式来处理并发,但这种方式并不是足够的高效。

可扩展性

  • Golang:Golang本身就是为了可扩展以及高并发场景设计的,所以没有太多的阻碍。
  • Nodejs:已经有很多使用Nodejs的人或者公司声称Node在大规模扩展的环境会有出现一些问题。可能Node可以在以后去修复这个问题。

开发成本

  • Golang:Golang算是一门比较新的语言并且还没有开始完全流行起来,所以需要做好要查阅大量资料以及手动配置的心理准备。
  • Nodejs:Nodejs有成百个第三库,上千份的指引,一大堆的第三方平台。庞大的Javascript社区近几年来一直有非常大的贡献,使其能在各种现实场景中都能完成任务。

错误处理

  • Golang:Golang要求程序进行常规的显式错误检查以及返回错误代码。这种做法看起来有点困难,然而这其实是一个更好的保持应用程序一致性的方法,这能使得应用程序更加可靠。
  • Nodejs:Nodejs使用throw/catch模型,当错误发生的时候立即触发进行捕获。这是一个更加传统的错误处理方法,很多编程语言也是使用相同的方法。所以应用程序的一致性就会有点欠缺。

晒图

2017年一个StackOverflow的开发者调查报告里面显示Golang位居前五在全世界顶级付费技术里面。

Golang vs Nodejs 基准任务性能

Golang: go version go1.10 linux/amd64

Node js :- v9.4.0

在比较了Golang与Nodejs各个方面以后,其实我们很难去说谁比谁更加出色,他们都有各自的闪光点以及缺点。在性能方面来说,Golang似乎比Nodejs更加可靠,但同时你也应该从项目大小,项目结构,以及业务类型等各种方面去比较作出考量。

译者补充

这部分内容并不是原文所有,本文翻译的内容并不代表我自己的观点,只是这篇原文在Medium上比较热门所以我翻译了过来可以供大家讨论。我本身是一个Nodejs以及前端的开发者超过3年了,最近在用Golang练习一些后端的项目也断断续续有几个月了。我非常喜欢Nodejs/Javascript的灵活性,虽然这也是被许多人诟病的地方。另外是这篇里面说到的并发比较只是简单说了Js的回调而没有提到事件循环,虽然Nodejs是单线程但正是有了事件循环,所以其在处理并发上也是足够高效的。但另一方面,Golang的Go rutines设计配合channel在并发情况性能确实非常高效,而正因为其不是单线程的特性,所以还会引申出共享资源竞争以及并发安全的问题,迫使我自己要去关注和处理以及考虑更多的情况在实际使用中,也可以因此接触到更多操作系统或者是更加底层的知识和概念,而不是简单的CRUD。我觉得Nodejs和Golang都有其各自的闪光点。我的建议是,如果你是一位Nodejs的后端工程师,希望另外再接触学习一门静态语言,希望更加底层地处理和熟悉高并发场景的话,我认为Golang是一个非常不错的选择。

本文转载自: 掘金

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

new一个对象的时候发生了什么

发表于 2019-10-24

一、引言

如你所知,Java是一门面向对象的编程语言。我们平常在写代码的时候也是在不停的操作各种对象,那么当你在写出User user = new User();这样一行代码的时候,JVM都做了些什么呢?

二、了解对象

1、内存布局

在Hotspot虚拟机中一个对象的内存布局分为三个部分:对象头、实例数据、对齐填充。

  • 对象头又有两部分的信息,第一部分是用于存储对象自身的运行数据(HashCode、GC分代年龄、锁状态标志等)。另一部分是类型指针,指向它的类元数据,虚拟机通过这个指针确定这个对象是哪个类的实例(如果使用句柄池方式则不会有)。如果是数组还会有一个记录数组长度的如下表所示:
内容 说明
Mark Word 对象的hashCode或锁信息等
Class Metadata Address 对象类型数据指针
Array length 数组长度
Mark Word是一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。各状态下的存储内容如下表所示:
标志位 状态
:——-: :————:
01 未锁定
00 轻量级锁定
10 重量级锁定
11 GC标记
01 可偏向
* 实例数据部分是真正存储的有效信息,就是在代码中定义的各种类型的字段内容。无论是父类继承下来的,还是在子类中的。
* 对齐填充不是必须存在的,仅仅起着占位符的作用,因为HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍。

2、对象的访问

Java程序中我们操作一个对象是通过指向这个对象的引用。我们都知道对象存在堆中,这个引用存在虚拟机栈中。那么引用通过什么方式去定位堆中对象的位置呢?

  • 直接指针法(HotSpot实现):引用中直接存储的就是堆中对象的地址。好处就是一次定位速度快,缺点是对象移动(GC时对象移动)引用本身需要修改。

  • 句柄法:Java堆中划分出一部分作为句柄池,引用存储的是对象的句柄地址,而句柄中包括了对象实例和类型的具体位置信息。好处是对象移动只会改变句柄中的实例数据指针,缺点是两次定位。

三、创建对象流程

上面介绍了对象的基本信息,现在来讲一讲创建对象的流程:

  1. 当虚拟机遇到一条new指令时,会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查代表的类是否已经被类加载器加载。如果没有被加载那么必须先执行这个类的加载。
  2. 类加载检查通过后,虚拟机将为新对象分配内存,对象所需内存的大小在类加载后便可以确定。
  3. 内存分配完成后,虚拟机需要将对象初始化为零值,保证对象的实例变量在代码中不赋初始值就能直接使用。类变量在类加载的准备阶段初始化为零值。
  4. 对对象头进行必要信息的设置,比如如何找到类的元数据信息、对象的HashCode、GC分代年龄等。
  5. 经过上述操作,一个新的对象已经产生,但是方法还没有执行,所有的字段都是零值。这时候需要执行方法(构造方法)把对象按照程序员的意愿进行初始化。类变量的初始化操作在类加载的初始化阶段方法完成

分配内存有两种方式:

  • Java堆内存是规整的(使用标记整理或带压缩的垃圾收集器),使用一个指针指向空闲位置,分配内存既将指针移动与分配大小相等的距离
  • 内存不是规整的(使用标记清除的垃圾收集器),虚拟机维护一个可用内存块列表,分配内存时从列表中找到一个足够大的内存空间划分给对象并更新可用内存列表。

无法找到足够的内存时会触发一次GC

分配内存时并发问题解决方案:

  • 对分配内存空间的动作进行同步操作—采用CAS失败重试的方式保证更新操作的原子性。
  • 每个线程在堆中预先分配一块小内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存就在它的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。通过-XX:+/-UseTLAB参数来设定。

四、创建对象指令重排序问题

A a = new A();

new一个对象的简单分解动作:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置引用指向分配的内存地址

其中2、3两步间会发生指令重排序,导致多线程时如果在初始化之前访问对象则会出现问题,单例模式的双重检测锁模式正是会存在这个问题。可以使用volatile来禁止指令重排序解决问题;

本文转载自: 掘金

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

MyCat教程一:MyCat的简单介绍 一、MyCat简介

发表于 2019-10-23

MyCat教程二:mysql主从复制实现

MyCat教程三:安装及配置介绍

MyCat教程四:实现读写分离

MyCat教程五:实现分库分表

MyCat教程六:全局序列号-全局主键的自增长

一、MyCat简介

  1. 什么是MyCat

  MyCat 是目前最流行的基于 java 语言编写的数据库中间件,是一个实现了 MySQL 协议的服务器,前端用户可以把它看作是一个数据库代理,用 MySQL 客户端工具和命令行访问,而其后端可以用 MySQL 原生协议与多个 MySQL 服务器通信,也可以用 JDBC 协议与大多数主流数据库服务器通信,其核心功能是分库分表。配合数据库的主从模式还可实现读写分离。

  MyCat 是基于阿里开源的 Cobar 产品而研发,Cobar 的稳定性、可靠性、优秀的架构和性能以及众多成熟的使用案例使得 MyCat 变得非常的强大。

  MyCat 发展到目前的版本,已经不是一个单纯的 MySQL 代理了,它的后端可以支持MySQL、SQL Server、Oracle、DB2、PostgreSQL 等主流数据库,也支持 MongoDB 这种新型NoSQL 方式的存储,未来还会支持更多类型的存储。而在最终用户看来,无论是那种存储方式,在 MyCat 里,都是一个传统的数据库表,支持标准的 SQL 语句进行数据的操作,这样一来,对前端业务系统来说,可以大幅降低开发难度,提升开发速度。

  MyCat 官网: www.mycat.io/

  1. MyCat的结构

在这里插入图片描述

  1. 使用MyCat的好处

3.1 数据量级

  单一的 MySQL 其数据存储量级和操作量级有限.

  Mycat 可以管理若干 MySQL 数据库,同时实现数据的存储和操作.

3.2 开源性质

  1. Mycat 是 java 编写的中间件. 开源,免费.
  2. 有非常多的人和组织对 Mycat 实行开发,维护,管理,更新.
  3. Mycat 版本提升较快,可以跟随环境发展.如果有问题,可以快速解决.
  4. Mycat 有开源网站和开源社区.且有官方发布的电子书籍.
  5. Mycat 是阿里原应用 corba 转型而来的.

3.3 市场应用

  MyCat 在互联网应用中占比非常高.

二、MyCat中的概念介绍

  1. 切分

  逻辑上的切分. 在物理层面,是使用多库[database],多表[table]实现的切分.

1.1 纵向切分/垂直切分

  就是把原本存储于一个库的数据存储到多个库上。

  由于对数据库的读写都是对同一个库进行操作,所以单库并不能解决大规模并发写入的问题。例如,我们会建立定义数据库 workDB、商品数据库 payDB、用户数据库 userDB、日志数据库 logDB 等,分别用于存储项目数据定义表、商品定义表、用户数据表、日志数据表等。

优点

  1. 减少增量数据写入时的锁对查询的影响。
  2. 由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单次查询所需的检索行数变少,减少了磁盘 IO,时延变短。

缺点:无法解决单表数据量太大的问题。

1.2横向切分/水平切分

  把原本存储于一个表的数据分块存储到多个表上。

  当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,进行划分,然后存储到多个结构相同的表,和不同的库上。例如,我们 userDB 中的 userTable 中数据量很大,那么可以把 userDB 切分为结构相同的多个 userDB:part0DB、part1DB 等,再将 userDB 上的 userTable,切分为很多userTable:userTable0、userTable1 等,然后将这些表按照一定的规则存储到多个 userDB 上。

优点

  1. 单表的并发能力提高了,磁盘 I/O 性能也提高了。
  2. 如果出现高并发的话,总表可以根据不同的查询,将并发压力分到不同的小表里面。

缺点:无法实现表连接查询。

  1. 逻辑库-Schema

  Mycat 中定义的 database.是逻辑上存在的.但是物理上是不存在的.主要是针对纵向切分提供的概念.

  1. 逻辑表-table

  Mycat 中定义的 table.是逻辑上存在,物理上是不存在的.主要是针对横向切分提供的概念.

  1. 默认端口

应用 端口
MySQL 3306
Mycat 8066
tomcat 8080
Oracle 1521
nginx 80
http 80
redis 6379
  1. 数据主机 - dataHost

  物理 MySQL 存放的主机地址.可以使用主机名,IP,域名定义.

  1. 数据节点 - dataNode

  配置物理的 database. 数据保存的物理节点.就是 database.

  1. 分片规则

  当控制数据的时候,如何访问物理 database 和 table.就是访问 dataHost 和 dataNode 的算法. 在 Mycat 处理具体的数据 CRUD 的时候,如何访问 dataHost 和 dataNode 的算法.如:哈希算法,crc32 算法等.


关注微信公众号【程序员的梦想】,专注于Java,SpringBoot,SpringCloud,微服务,Docker以及前后端分离等全栈技术。
在这里插入图片描述

本文转载自: 掘金

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

面试:原来Redis常用的五种数据类型底层结构是这样的

发表于 2019-10-22

关注我,可以获取最新知识、经典面试题以及微服务技术分享

  在Redis中会涉及很多数据结构,比如SDS,双向链表、字典、压缩列表、整数集合等等。Redis会基于这些数据结构自定义一个对象系统,而且自定义的对象系统有很多好处。

通过对以下的Redis对象系统的学习,可以了解Redis设计原理以及初衷,为了我们在使用Redis的时候,更加能够理解到其原理和定位问题。

Redis 对象

Redis基于上述的数据结构自定义一个Object 系统,Object结构:

1
2
3
4
5
6
7
8
9
10
复制代码redisObject结构:
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
…..
}

Object 系统包含五种Object:

  • String:字符串对象
  • List:列表对象
  • Hash:哈希对象
  • Set:集合对象
  • ZSet:有序集合

Redis使用对象来表示数据库中的键和值,即每新建一个键值对,至少创建有两个对象,而且使用对象的具有以下好处:

  1. redis可以在执行命令前会根据对象的类型判断一个对象是否可以执行给定的命令
  2. 针对不同的使用场景,为对象设置不同的数据结构实现,从而优化对象的不同场景夏的使用效率
  3. 对象系统还可以基于引用计数计数的内存回收机制,自动释放对象所占用的内存,或者还可以让多个数据库键共享同一个对象来节约内存。
  4. redis对象带有访问时间记录信息,使用该信息可以进行优化空转时长较大的key,进行删除!

对象的ptr指针指向对象的底层现实数据结构,而这些数据结构由对象的encoding属性决定,对应关系:

编码常量 编码对应的底层数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双向链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典

每种Object对象至少有两种不同的编码,对应关系:

类型 编码 对象
String int 整数值实现
String embstr sds实现 <=39 字节
String raw sds实现 > 39字节
List ziplist 压缩列表实现
List linkedlist 双端链表实现
Set intset 整数集合使用
Set hashtable 字典实现
Hash ziplist 压缩列表实现
Hash hashtable 字典使用
Sorted set ziplist 压缩列表实现
Sorted set skiplist 跳跃表和字典

String 对象

字符串对象编码可以int 、raw或者embstr,如果保存的值为整数值且这个值可以用long类型表示,使用int编码,其他编码类似。

比如:int编码的String Object

1
2
3
4
复制代码redis> set number 520 
ok
redis> OBJECT ENCODING number
"int"

String Object结构:

file

String 对象之间的编码转换

int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。

比如:对int编码的字符串对象进行append命令时,就会使得原来是int变为raw编码字符串

List对象

list对象可以为ziplist或者为linkedlist,对应底层实现ziplist为压缩列表,linkedlist为双向列表。

1
复制代码Redis>RPUSH numbers “Ccww” 520 1

用ziplist编码的List对象结构:

file

用linkedlist编码的List对象结构:

file

List对象的编码转换:

当list对象可以同时满足以下两个条件时,list对象使用的是ziplist编码:

  1. list对象保存的所有字符串元素的长度都小于64字节
  2. list对象保存的元素数量小于512个,
    不能满足这两个条件的list对象需要使用linkedlist编码。

Hash对象

Hash对象的编码可以是ziplist或者hashtable
其中,ziplist底层使用压缩列表实现:

  • 保存同一键值对的两个节点紧靠相邻,键key在前,值vaule在后
  • 先保存的键值对在压缩列表的表头方向,后来在表尾方向

hashtable底层使用字典实现,Hash对象种的每个键值对都使用一个字典键值对保存:

  • 字典的键为字符串对象,保存键key
  • 字典的值也为字符串对象,保存键值对的值

比如:HSET命令

1
2
3
4
5
6
7
8
复制代码redis>HSET author name  "Ccww"
(integer)

redis>HSET author age 18
(integer)

redis>HSET author sex "male"
(integer)

ziplist的底层结构:

file

hashtable底层结构:

file

Hash对象的编码转换:

当list对象可以同时满足以下两个条件时,list对象使用的是ziplist编码:

  1. list对象保存的所有字符串元素的长度都小于64字节
  2. list对象保存的元素数量小于512个,
    不能满足这两个条件的hash对象需要使用hashtable编码

Note:这两个条件的上限值是可以修改的,可查看配置文件hash-max-zaiplist-value和hash-max-ziplist-entries

Set对象:

Set对象的编码可以为intset或者hashtable

  • intset编码:使用整数集合作为底层实现,set对象包含的所有元素都被保存在intset整数集合里面
  • hashtable编码:使用字典作为底层实现,字典键key包含一个set元素,而字典的值则都为null
    inset编码Set对象结构:
1
复制代码	redis> SAD number  1 3 5

file

hashtable编码Set对象结构:

1
复制代码redis> SAD Dfruits  “apple”  "banana" " cherry"

file

Set对象的编码转换:

使用intset编码:

  1. set对象保存的所有元素都是整数值
  2. set对象保存的元素数量不超过512个
    不能满足这两个条件的Set对象使用hashtable编码

ZSet对象

ZSet对象的编码 可以为ziplist或者skiplist
ziplist编码,每个集合元素使用相邻的两个压缩列表节点保存,一个保存元素成员,一个保存元素的分值,然后根据分数进行从小到大排序。

ziplist编码的ZSet对象结构:

1
复制代码Redis>ZADD price 8.5 apple 5.0 banana 6.0 cherry

file

skiplist编码的ZSet对象使用了zset结构,包含一个字典和一个跳跃表

1
2
3
4
5
6
7
8
复制代码Type struct zset{

Zskiplist *zsl;
dict *dict;
...
}

skiplist编码的ZSet对象结构

file

ZSet对象的编码转换

当ZSet对象同时满足以下两个条件时,对象使用ziplist编码

  1. 有序集合保存的元素数量小于128个
  2. 有序集合保存的所有元素的长度都小于64字节
    不能满足以上两个条件的有序集合对象将使用skiplist编码。

Note: 可以通过配置文件中zset-max-ziplist-entries和zset-max-ziplist-vaule

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!

欢迎关注公众号【Ccww技术博客】,原创技术文章第一时间推出

本文转载自: 掘金

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

从 StringgetBytes 理解 Java 编码和解

发表于 2019-10-22

背景

周末一直在想 String.getBytes 原理。查阅了一些资料,终于用代码验证了自己的想法。本篇不会涉及太多源码相关的知识。

知识点

  • Unicode 和 UTF-8 的关系
  • 原码,反码,补码
  • Java 字符串和 Unicode 的关系
  • 理解 String.getBytes,解决乱码问题
  • Demo 验证猜想

用到的工具

进制之间的相互转换

Unicode 编码表

GBK 编码表

Unicode 官网

原码,补码,反码

因为原码,补码,反码比较简单,我这里粘贴一个例子进行展示。

图片内容来源:www.cnblogs.com/yinzhengjie…

Unicode 和 UTF-8 的关系

Uincode 是一个字符集。它规定了我们使用到的字或符号的码点(code point)。码点使用 16 进制保存。

Uincode 字符集规定 一 的码点为 4E00。

Uincode 字符集规定 丁 的码点为 4E01。

计算机呢只能识别二进制的 0 和 1。而 UTF-8 指的是编码规则,规定码点怎么保存成二进制。

还有别的 Unicode 编码规则,UTF-16 和 UTF-32。

十进制 Unicode符号范围(十六进制) UTF-8编码方式(二进制)
0-127 0000 0000-0000 007F 0xxxxxxx
128-2047 0000 0080-0000 07FF 110xxxxx 10xxxxxx
2048-65535 0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
65536-1114111 0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

上述表格简单描述了Unicode 按 UTF-8 编码的格式。

  • 首先将 16 进制的码点,通过进制转换 为十进制
  • 然后使用十进制的数字查找上述表格处于哪个范围中,得出编码规则。
  • 然后将码点转换为 2 进制,从低位到高位替换 x 即可得到字二进制的原码
  • 将二进制的原码转换为补码存储。

java 内存中的字符串采用的是 unicode 编码,也就是内编码。我们可以从 unicode 转变为 GBK 或 UTF-8 等其它规则。

代码验证猜想

以赵为例子讲解。

赵的码点为:8D75

16 进制的码点转换为 10 进制:36213

36213 处于 2048-65535 ,得出对应的 UTF-8 编码格式为:1110xxxx 10xxxxxx 10xxxxxx

赵的 16 进制码点 8D75 转换为二进制 1000

将二进制填充在 1110xxxx 10xxxxxx 10xxxxxx 中的 x 中,不足的补 0.

11101000 10110101 10110101。

对三个字节分别求补码为:

原码:11101000 10110101 10110101

补码:10011000 11001011 11001011

补码对应 java 中的字节数组为:{-24,-75,-75}

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Test
public void run454() throws UnsupportedEncodingException {
String str ="赵";
final byte[] bytes = str.getBytes("UTF-8");
StringBuilder stringBuilder =new StringBuilder();

for (byte aByte : bytes) {
stringBuilder.append(aByte).append(",");
}

System.out.println(stringBuilder.toString());
}
  • 再加一个例子:

且的码点:4E14

16 进制的码点转换为 10 进制:19988

19988 处于 2048-65535 ,得出对应的 UTF-8 编码格式为:1110xxxx 10xxxxxx 10xxxxxx

16 进制的码点转换成二进制:100111000010100

原码:11100100 10111000 10010100

补码:10011100 11001000 11101100

补码对应的字节数组为:{-28,-72,-108}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@Test
public void run43() throws UnsupportedEncodingException {

// {-28,-72,-108}
String str ="且";
final byte[] bytes = str.getBytes("UTF-8");
StringBuilder stringBuilder =new StringBuilder();

for (byte aByte : bytes) {
stringBuilder.append(aByte).append(",");
}

System.out.println(stringBuilder.toString());
}

GBK 转码

赵的 GBK 码点为:D5D4
十六进制码点转换为二进制:11010101 11010100
源码:11010101 11010100
补码:10101011 10101100

补码对应的字节数组为:{-43,-44}

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Test
public void run454() throws UnsupportedEncodingException {
String str ="赵";
final byte[] bytes = str.getBytes("GBK");
StringBuilder stringBuilder =new StringBuilder();

for (byte aByte : bytes) {
stringBuilder.append(aByte).append(",");
}
// -43,-44
System.out.println(stringBuilder.toString());
}

JAVA 中乱码问题

java 字符或字符串采用 uincode 作为内编码。

1
2
3
4
5
6
7
8
复制代码@Test
public void run44() {
String str="\u0c2c";
// బ
System.out.println(str);
// ✈
System.out.println("\u2708");
}

编码:字符串到字节。

解码:字节到字符串。

当我们读取文件的时候实际读取的是字节。然后根据文件的编码格式,将字节解码成字符串。乱码问题容易出现的地方就是这里。

不要妄想将一个乱码的字符串变成一个非乱码的。这个思路是错误的。应该从乱码之前的字节着手处理。

1
2
3
4
5
6
7
复制代码@Test
public void run100() throws UnsupportedEncodingException {
String str = "张";
final byte[] gbks = str.getBytes("GBK");
final String s = new String(gbks, "UTF-8");
System.out.println(s);
}

上述例子中的 s 已经乱码了,当你操作这个 s 获取字节也是乱码的。

因此思路是操作 gbks 转换解码方式获取字符串。

参考资料

阮一峰字符编码笔记

本文转载自: 掘金

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

这道Java基础题真的有坑!我求求你,认真思考后再回答。 本

发表于 2019-10-22

本文首发于公众号,关注文末公众号,阅读体验更佳。

这是个人第10篇原创文章

全文共计7362个字,46张图。分析的较为详尽,并进行了相关知识点的扩展,所以篇幅较长,建议转发朋友圈或者自己收藏起来,慢慢阅读。

本文目录

一.题是什么题?

二.阿里Java开发规范。

1
2
3
复制代码     2.1 正例代码。
2.2 反例代码。
三.层层揭秘,为什么发生异常了呢?
1
2
3
4
5
6
ruby复制代码    3.1 第一层:异常信息解读。
3.2 第二层:抛出异常的条件解读。
3.3 第三层:什么是modCount?它是干啥的?什么时候发生变化?
3.4 第四层:什么是expectedModCount?它是干啥的?什么时候发生变化?
3.5 第五层:组装线索,直达真相。
四.这题的坑在哪?
1
2
3
processing复制代码    4.1 回头再看。
4.2 还有一个骚操作。
五.线程安全版的ArrayList。

六.总结一下。

七.回答另外一个面试题。

八.扩展阅读。

1
2
3
4
llvm复制代码     7.1 fail-fast机制和safe-fast机制。
7.2 Java语法糖。
7.3 阿里Java开发手册。
九.最后说一句。

一.题是什么题?

我第一次遇到这个题的时候,是在一个微信群里,阿里著名的”Java劝退师”小马哥抛出了这样的一个问题:file

然后大家纷纷给出了自己的见解(注:删除了部分聊天记录):

file

后面在另外的群里聊天的时候(注:删除了部分聊天记录),我也抛出了这样的问题:

file

总结一下图片中的各种回答:

1.什么也不会发生,remove之后,list中的数据会被清空。

2.remove的方法调用错误,入参应该是index(数组下标)。

3.并发操作的时候会出现异常。

4.会发生ConcurrentModifyException。

你的答案又是什么呢?

在这里,我先不说正确的答案是什么,也先不评价这些回答是对是错,我们一起去探索真相,寻找答案。

二.阿里Java开发规范

有人看到题的第一眼(没有认真读题),就想起了阿里java开发手册(先入为主),里面是这样说的:file

正是因为大多数人都知道并且读过这个规范(毕竟是业界权威)。所以呼声最高的答案是【会发生ConcurrentModifyException】。因为他们知道阿里java开发手册里面是强制要求:不要在foreach循环里面进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

但是不能因为他是权威,我们就全盘接受吧?

2.1 正例代码

所以我们眼见为实,先把手册里面提到的【正例代码】跑一下,如下:

file

细心的读者可能发现了:咦,这个代码的22行为啥颜色不一样呢?

我帮你看看。

file

替换之后的代码是这样的:

file

从上面我们可以得到一个结论…….

等等,到这一步你就想得到结论了?你不对【一行代码为什么就替换了七行代码】好奇吗?

看到真相的时候,有时候再往前一步就是本质了。

源码之下无秘密,我再送你一张图,JDK1.8中Collection.removeIf的源码:

file

好了,已经到源码级别了,从这里我们验证了,阿里java开发手册里面的正例是对的,而且我还想给他加上一句:如果你的JDK版本是1.8以上,没有并发访问的情况下,可以使用Collection.removeIf(Predicate filter)方法。使代码更加优雅。

2.2 反例代码

接下来我们看看【反例代码】的运行结果:

file

从执行结果来看,和我们预期的结果是一致。看着没有问题呀?

但是你别忘了,下面还有一句话啊:

file

我们执行试一试:

file

什么情况?真的是”出乎意料”啊!

把删除元素的条件从【公众号】修改为【why技术】就发生了异常:java.util.ConcurrentModificationException

file

三.层层揭秘,为什么发生了异常呢?

我们现在明白为什么阿里强制要求不要在foreach循环里面进行元素的remove/add操作,因为会发生异常了。

但是开发手册里面并没有告诉你,为什么会发生异常。需要我们自己层层深入,积极探索。

3.1 第一层:异常信息解读

所以这一小节我们就一起探索,为什么会发生异常。我们再解析一下程序的运行结果,如下:

file

正如上图里面异常信息的体现,异常是在代码的第21行触发的。而代码的第21行,是一个foreach循环。foreach循环是Java的语法糖,我们可以从编译后的class文件中看出,如下图所示:

file

请注意图中的第26行代码:list.remove(item) (这句话很关键!!!)很关键,很重要,后面会讲到。

这也解释了,异常信息里面的这一个问题:

file

好了,到这一步,我们把异常信息都解读完毕了。

3.2 第二层:抛出异常的条件解读

我再看看真实抛出异常的那一个方法:

file

很简单,很清晰的四行代码。抛出异常的条件是:modCount !=expectedModCount

所以,我们需要解开的下两层面纱就是下面两大点:

第一:什么是modCount?它是干啥的?什么时候发生变化?

第二:什么是expectedModCount?它是干啥的?什么时候发生变化?

3.3 第三层:什么是modCount?它是干啥的?什么时候发生变化?

先来第一个:什么是modCount?

file

modCount上的注释很长,我只截取了最后一段。在这一段中,提到了两个关键点。

1.modCount这个字段位于java.util.AbstractList抽象类中。

2.modCount的注释中提到了”fail-fast”机制。

3.如果子类希望提供”fail-fast”机制,需要在add(int,E)方法和remove(int)方法中对这个字段进行处理。

4.从第三点我们知道了,在提供了”fail-fast”机制的容器中(比如ArrayList),除了文中示例的remove(Obj)方法会导致ConcurrentModificationException异常,add及其相关方法也会导致异常。

知道了什么是modCount。那modCount是干啥的呢?

在提供了”fail-fast”机制的集合中,modCount的作用是记录了该集合在使用过程中被修改的次数。

证据就在源码里面,如下:

这是java.util.ArrayList#add(int, E)方法的源码截图:

file

这是java.util.ArrayList#remove(int)方法的源码截图:

file

注:这里不讨论手动设置为null是否对GC有帮助,我个人认为,在这里有这一行代码并没有坏处。在实际开发过程中,一般不需要考虑到这点。

同时,上面的源码截图也回答了这一层的最后一个问题:它什么时候被修改?

拿ArrayList来说,当调用add相关和remove相关方法时,会触发modCount++操作,从而被修改。

好了,通过上面的分析,我们知道了什么是modCount和modCount是干啥的。准备进入第四层。

3.4 第四层:什么是expectedModCount?它是干啥的?什么时候发生变化?

接下来:什么是expectedModCount?

expectedModCount是ArrayList中一个名叫Itr内部类的成员变量。

file

第二问:expectedModCount它是干啥的:

它代表的含义是在这个迭代器中,预期的修改次数

第三问:expectedModCount什么时候发生变化?

file

情况一:从上图中也可以看出当Itr初始化的时候,会对expectedModCount字段赋初始值,其值等于modCount。

情况二:如下图所示,调用Itr的remove方法后会再次把modCount的值赋给expectedModCount。

换句话说就是:调用迭代器的remove会维护expectedModCount=modCount。(这句话很关键!!!)

好了分析到了这里,我们知道了下面这个六连击:

1.什么是modCount?

2.modCount是干啥的?

3.modCount什么时候发生变化?

4.什么是expectedModCount?

5.expectedModCount是干啥的?

6.expectedModCount什么时候发生变化?

3.5 第五层:组装线索,直达真相

为什么发生了异常呢?

如果说前四层是线索的话,真相其实已经隐藏在线索里面了。我带你梳理一下:

【第一层:异常信息解读】中说到:

file

【第二层:抛出异常的条件解读】中说到:

file

【第三层:什么是modCount?它是干啥的?什么时候发生变化?】中说到:

file

【第四层:什么是expectedModCount?它是干啥的?什么时候发生变化?】中说到:

file

为什么发生了异常呢?我想你大概已经有了一个答案了,我再去Debug一下,为了方便演示,我们去掉语法糖,程序修改如下:

file

并确认一下这个循环体会执行三次,如下:

file

第一次循环

file

第一次循环取出的【公众号】,不满足条件if(“why技术”.equals(item)),不会触发list.remove(Obj)方法。

第二次循环

file

如图所示,第二次循环取到了“why技术”。满足条件if(“why技术”.equals(item)),会触发list.remove(Obj)方法,如下所示:

file

第三次循环

file

总结一下在foreach循环里面进行元素的remove/add操作抛出异常的真相:

因为foreach循环是Java的语法糖,经过编译后还原成了迭代器。

但是从经过编译后的代码的第26行可以看出,remove方法的调方是list,而不是迭代器。

经过前面的源码分析我们知道,由于ArrayList的”fail-fast”机制,调用remove方法会触发【modCount++】操作,对expectedModCount没有任何操作。只有调用迭代器的remove方法,才会维护expectedModCount=modCount。

所以调用了list的remove方法后,再调用Itr的next方法时,导致了expectedModCount!=modCount,抛出异常。

四.这题的坑在哪里?

前面讲了阿里开发手册。讲了在foreach循环里面进行元素的remove/add为什么会发生异常。有了这些铺垫之后。

4.1 回头再看

我们再回过头来看小马哥出的这个题:

file

我靠,这乍一看,foreach循环里面调用list.remove(obj)。我们刚刚分析过,会抛出ConcurrentModificationException异常。

你要这样答,你就进了小马哥的坑了。

file

这个题的坑在这三个点里面。小马哥并没有说这个list是ArrayList吧?如果你没有认真审题,先入为主的默认了这个list就是ArrayList。第一步就错了。

这是真正的高手,借力打力。借阿里开发手册的力,让你第一步就走错。

请看下面这张图:

file

当使用CopyOnWriteArrayList的时候,程序正常执行。

4.2 还有一个骚操作

既然我们知道为什么会抛出异常,也知道怎么不抛出异常,List本来就是一个接口,那我们是不是可以实现这个接口,弄一个自定义的List呢?

比如下面的这个WhyTechnologyList,就是我自己的List,狸猫换太子,这操作,够”骚”啊。

file

只有掌握了原理,我们想怎么玩就怎么玩。

五.线程安全版的ArrayList

CopyOnWriteArrayList是什么?我们看一下源码注释上面是怎么说的:

file

相对于ArrayList而言,CopyOnWriteArrayList集合是线程安全的容器。在遍历的时候,由于它操作是数组的”快照”,”快照”不会发生变化。所以它不需要额外加锁,也不会抛出ConcurrentModificationException异常。

我们主要看一下,示例程序中用到的三个方法,add(E e)、next()、remove(Obj)

先看add(E e)方法:

file

我们看一下它的next()方法:

file

再看一下它的remove(Obj)方法:

file

next、remove都是操作的快照,并没有看到ArrayList里面的modCount和expectedModCount。所以它没有抛出ConcurrentModificationException

之前看小马哥说的这句话的时候还不太明白集合和一致性之间的关系(老问题,还是先入为主,一说到一致性首先想到的是缓存和数据库之间的一致性)。

file

但是当我阅读源码,从add方法可以看出CopyOnWriteArrayList并不保证数据的实时一致性。只能保证最终一致性。

同时我们从源码中可以看出CopyOnWriteArrayList增删改数据的时候需要搞一个”快照”,这一点是比较耗内存的,使用过程中需要注意。

六.总结一下

我们再回到最开始的地方,看看大家的回答:

1.什么也不会发生,remove之后,list中的数据会被清空。

2.remove的方法调用错误,入参应该是index(数组下标)。

3.并发操作的时候会出现异常。

4.会发生ConcurrentModifyException。

现在,你知道这些回答的问题在哪里了吧?这一部分的总结也很简单,上一个对比图就好了:

file

file

七.回答另外一个面试题

现在面试官经常问的一个问题,你读过源码吗?

咦,巧了。你看了这篇文章,就相当于了读了ArrayList和CopyOnWriteArrayList的部分源码。

那你就可以这样回答啦:我之前看阿里Java开发手册的时候看到一条规则是不要在foreach循环里面进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。我对这条规则很感兴趣,所以我对其进行了深入的研究,阅读了ArrayList和CopyOnWriteArrayList的部分源码。

如果碰巧面试官也读过这块源码,这个问题,你们可以相谈甚欢。如果面试官没有读过这块源码,你可以给他讲的明明白白。

当然,还有一个前提是:我希望你读完这篇文章后,如果是第一次知道这个知识点,那你可以自己实际操作一下。

看懂了是一回事,自己再实际操作一下,是另外一回事。

八.扩展阅读

8.1 fail-fast和fail-safe机制

文中多次提到了”fail-fast”机制(快速失败),与其对应的还有”fail-safe”机制(失败安全)。

这种机制是一种思想,它不仅仅是体现在Java的集合中。在我们常用的rpc框架Dubbo中,在集群容错时也有相关的实现。

file

Dubbo 主要提供了这样几种容错方式:

Failover Cluster - 失败自动切换

Failfast Cluster - 快速失败

Failsafe Cluster - 失败安全

Failback Cluster - 失败自动恢复

Forking Cluster - 并行调用多个服务提供者

如果对这两种机制感兴趣的朋友可以查阅相关资料,进行了解。如果想要了解Dubbo的集群容错机制,可以看官方文档,地址如下:http://dubbo.apache.org/zh-cn/docs/source*code*guide/cluster.html

8.2 Java语法糖

文中说到foreach循环的时候提到了Java的语法糖。如果对这一块有兴趣的读者,可以在网上查阅相关资料,也可以看看《深入理解Java虚拟机》的第10.3节,有专门的介绍。

file

书中说到:

总而言之,语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”,但我们也应该去了解这些“小把戏”背后的真实世界,那样才能利用好它们,而不是被它们所迷惑。

关注公众号并回复关键字【Java】。即可获得此书的电子版。

8.3 阿里Java开发手册

阿里的孤尽大佬作为主要作者写的这本《阿里Java开发手册》,可以说是呕心沥血推出的业界权威,非常值得阅读。读完此书,你不仅能够获得很多干货,甚至你还能读出一点技术情怀在里面。

对于技术情怀,孤尽大佬是这样的说的:

热爱、思考、卓越。热爱是一种源动力,而思考是一个过程,而卓越是一个结果。如果给这三个词加一个定语,使技术情怀更加立体、清晰地被解读,那就是奉献式的热爱,主动式的思考,极致式的卓越。

关注公众号并回复关键字【Java】。即可获得此书的电子版。

九.最后说一点

这篇文章写之前我一直在纠结,因为感觉这个知识点其实我已经掌握了,那我还有写的必要吗?我在写的这个过程中还能收获一些东西吗?

但是在写的过程中,我翻阅了大量的源码,虽然之前已经看过,但是没有这样一行一行仔细的去分析。之前只是一个大概的模糊的影像,现在具象化清晰了起来,在这个过程中,我还是学到了很多很多。

其实想到写什么内容并不难,难的是你对内容的把控。关于技术性的语言,我是反复推敲,查阅大量文章来进行证伪,总之慎言慎言再慎言,毕竟做技术,我认为是一件非常严谨的事情,我常常想象自己就是在故宫修文物的工匠,在工匠精神的认知上,目前我可能和他们还差的有点远,但是我时常以工匠精神要求自己。就像我之前表达的:对于技术文章(因为我偶尔也会荒腔走板的聊一聊生活,写一写书评,影评),我尽量保证周推,全力保证质量。

文中提到的两本书《深入理解Java虚拟机》和《阿里Java开发手册》是两本非常优秀,值得反复阅读的工具书,可以关注我后,在后台发送java,即可获得电子书。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

如果你觉得文章还不错,你的点赞、留言、转发、分享、赞赏就是对我最大的鼓励。

另外,如果小马哥本尊能读到这个文章,读到这段话,我想在这里表达对他的敬意。同时也想催更一下:小马哥,每日一问好久没更新啦,非常怀恋那种被”坑”的明明白白的感觉!

以上。

谢谢您的阅读,感谢您的关注。

欢迎关注公众号【why技术】。在这里我会分享一些技术相关的东西,主攻java方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。

公众号-why技术

本文转载自: 掘金

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

弄明白CMS和G1,就靠这一篇了

发表于 2019-10-21

在开始介绍CMS和G1前,我们可以剧透几点:

  • 根据不同分代的特点,收集器可能不同。有些收集器可以同时用于新生代和老年代,而有些时候,则需要分别为新生代或老年代选用合适的收集器。一般来说,新生代收集器的收集频率较高,应选用性能高效的收集器;而老年代收集器收集次数相对较少,对空间较为敏感,应当避免选择基于复制算法的收集器。
  • 在垃圾收集执行的时刻,应用程序需要暂停运行。
  • 可以串行收集,也可以并行收集。
  • 如果能做到并发收集(应用程序不必暂停),那绝对是很妙的事情。
  • 如果收集行为可控,那也是很妙的事情。

CMS和G1作为垃圾收集器里的大杀器,是需要好好弄明白的,而且面试中也经常被问到。

希望大家带着下面的问题进行阅读,有目标的阅读,收获更多:

  1. 为什么没有一种牛逼的收集器像银弹一样适配所有场景?
  2. CMS的优点、缺点、适用场景?
  3. 为什么CMS只能用作老年代收集器,而不能应用在新生代的收集?
  4. G1的优点、缺点、适用场景?

1 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的。

CMS收集器仅作用于老年代的收集,是基于标记-清除算法的,它的运作过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。

CMS以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。只将那些必需STW才能执行的操作单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就可以完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和重新标记),达到了近似并发的目的。

CMS收集器优点:并发收集、低停顿。

CMS收集器缺点:

  • CMS收集器对CPU资源非常敏感。
  • CMS收集器无法处理浮动垃圾(Floating Garbage)。
  • CMS收集器是基于标记-清除算法,该算法的缺点都有。

CMS收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供CMS版本。

另外要补充一点,JVM在暂停的时候,需要选准一个时机。由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点的概念。

安全点(Safepoint)

安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。

安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。

程序运行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。

对于安全点,另一个需要考虑的问题就是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。

两种解决方案:

  • 抢先式中断(Preemptive Suspension)

抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。

  • 主动式中断(Voluntary Suspension)

主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域

指在一段代码片段中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也可以把Safe Region看作是被扩展了的Safepoint。

2 G1收集器

G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。

G1与CMS的特征对比如下:

特征 G1 CMS
并发和分代 是 是
最大化释放堆内存 是 否
低延时 是 是
吞吐量 高 低
压实 是 否
可预测性 强 弱
新生代和老年代的物理隔离 否 是

G1具备如下特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 分代收集
  • 空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。在堆的结构设计时,G1打破了以往将收集范围固定在新生代或老年代的模式,G1将堆分成许多相同大小的区域单元,每个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成如下图所示:

G1堆的Region布局.png

G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region,这一点可以参看如下源码。其实这个数字既可以手动调整,G1也会根据堆大小自动进行调整。

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
复制代码#ifndef SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP
#define SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

#include "memory/allocation.hpp"

class HeapRegionBounds : public AllStatic {
private:
// Minimum region size; we won't go lower than that.
// We might want to decrease this in the future, to deal with small
// heaps a bit more efficiently.
static const size_t MIN_REGION_SIZE = 1024 * 1024;

// Maximum region size; we don't go higher than that. There's a good
// reason for having an upper bound. We don't want regions to get too
// large, otherwise cleanup's effectiveness would decrease as there
// will be fewer opportunities to find totally empty regions after
// marking.
static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;

// The automatic region size calculation will try to have around this
// many regions in the heap (based on the min heap size).
static const size_t TARGET_REGION_NUMBER = 2048;

public:
static inline size_t min_size();
static inline size_t max_size();
static inline size_t target_number();
};

#endif // SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。

对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:

  • 实时数据占用了超过半数的堆空间;
  • 对象分配率或“晋升”的速度变化明显;
  • 期望消除耗时较长的GC或停顿(超过0.5——1秒)。

原文如下:
Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.

  • More than 50% of the Java heap is occupied with live data.
  • The rate of object allocation rate or promotion varies significantly.
  • Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)

G1收集的运作过程大致如下:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

全局变量和栈中引用的对象是可以列入根集合的,这样在寻找垃圾时,就可以从根集合出发扫描堆空间。在G1中,引入了一种新的能加入根集合的类型,就是记忆集(Remembered Set)。Remembered Sets(也叫RSets)用来跟踪对象引用。G1的很多开源都是源自Remembered Set,例如,它通常约占Heap大小的20%或更高。并且,我们进行对象复制的时候,因为需要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。

image.png

卡表(Card Table)

有个场景,老年代的对象可能引用新生代的对象,那标记存活对象的时候,需要扫描老年代中的所有对象。因为该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是得又做全堆扫描?成本太高了吧。

HotSpot给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。

在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。

想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。

卡表能用于减少老年代的全堆空间扫描,这能很大的提升GC效率。

我们可以看下官方文档对G1的展望(这段英文描述比较简单,我就不翻译了):

Future:
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.

3 总结

查了下度娘有关G1的文章,绝大部分文章对G1的介绍都是停留在JDK7或更早期的实现很多结论已经存在较大偏差了,甚至一些过去的GC选项已经不再推荐使用。举个例子,JDK9中JVM和GC日志进行了重构,如PrintGCDetails已经被标记为废弃,而PrintGCDateStamps已经被移除,指定它会导致JVM无法启动。

本文对CMS和G1的介绍绝大部分内容也是基于JDK7,新版本中的内容有一点介绍,倒没做过多介绍(本人对新版本JVM还没有深入研究),后面有机会可以再出专门的文章来重点介绍。

4 参考

《深入理解Java虚拟机》
《HotSpot实战》
《极客时间专栏》

本文转载自: 掘金

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

业务复杂=if else?刚来的大神竟然用策略+工厂彻底干掉

发表于 2019-10-21

​对于业务开发来说,业务逻辑的复杂是必然的,随着业务发展,需求只会越来越复杂,为了考虑到各种各样的情况,代码中不可避免的会出现很多if-else。一旦代码中if-else过多,就会大大的影响其可读性和可维护性。

首先可读性,不言而喻,过多的if-else代码和嵌套,会使阅读代码的人很难理解到底是什么意思。尤其是那些没有注释的代码。

其次是可维护性,因为if-else特别多,想要新加一个分支的时候,就会很难添加,极其容易影响到其他的分支。

笔者曾经看到过一个支付的核心应用,这个应用支持了很多业务的线上支付功能,但是每个业务都有很多定制的需求,所以很多核心的代码中都有一大坨if-else。

每个新业务需要定制的时候,都把自己的if放到整个方法的最前面,以保证自己的逻辑可以正常执行。这种做法,后果可想而知。

其实,if-else是有办法可以消除掉的,其中比较典型的并且使用广泛的就是借助策略模式和工厂模式,准确的说是利用这两个设计模式的思想,彻底消灭代码中的if-else。

本文,就结合这两种设计模式,介绍如何消除if-else,并且,还会介绍如何和Spring框架结合,这样读者看完本文之后就可以立即应用到自己的项目中。

本文涉及到一些代码,但是作者尽量用通俗的例子和伪代码等形式使内容不那么枯燥。

恶心的if-else假设我们要做一个外卖平台,有这样的需求:

1、外卖平台上的某家店铺为了促销,设置了多种会员优惠,其中包含超级会员折扣8折、普通会员折扣9折和普通用户没有折扣三种。

2、希望用户在付款的时候,根据用户的会员等级,就可以知道用户符合哪种折扣策略,进而进行打折,计算出应付金额。

3、随着业务发展,新的需求要求专属会员要在店铺下单金额大于30元的时候才可以享受优惠。

4、接着,又有一个变态的需求,如果用户的超级会员已经到期了,并且到期时间在一周内,那么就对用户的单笔订单按照超级会员进行折扣,并在收银台进行强提醒,引导用户再次开通会员,而且折扣只进行一次。

那么,我们可以看到以下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public BigDecimal calPrice(BigDecimal orderPrice, String buyerType) {

if (用户是专属会员) {
if (订单金额大于30元) {
returen 7折价格;
}
}

if (用户是超级会员) {
return 8折价格;
}

if (用户是普通会员) {
if(该用户超级会员刚过期并且尚未使用过临时折扣){
临时折扣使用次数更新();
returen 8折价格;
}
return 9折价格;
}
return 原价;
}

以上,就是对于这个需求的一段价格计算逻辑,使用伪代码都这么复杂,如果是真的写代码,那复杂度可想而知。

这样的代码中,有很多if-else,并且还有很多的if-else的嵌套,无论是可读性还是可维护性都非常低。

那么,如何改善呢?

策略模式接下来,我们尝试引入策略模式来提升代码的可维护性和可读性。

首先,定义一个接口:

1
2
3
4
5
6
7
8
9
10
复制代码/**
* @author mhcoding
*/
public interface UserPayService {

/**
* 计算应付价格
*/
public BigDecimal quote(BigDecimal orderPrice);
}

接着定义几个策略类:

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
复制代码/**
* @author mhcoding
*/
public class ParticularlyVipPayService implements UserPayService {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if (消费金额大于30元) {
return 7折价格;
}
}
}

public class SuperVipPayService implements UserPayService {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
return 8折价格;
}
}

public class VipPayService implements UserPayService {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if(该用户超级会员刚过期并且尚未使用过临时折扣){
临时折扣使用次数更新();
returen 8折价格;
}
return 9折价格;
}
}

引入了策略之后,我们可以按照如下方式进行价格计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码/**
* @author mhcoding
*/
public class Test {

public static void main(String[] args) {
UserPayService strategy = new VipPayService();
BigDecimal quote = strategy.quote(300);
System.out.println("普通会员商品的最终价格为:" + quote.doubleValue());

strategy = new SuperVipPayService();
quote = strategy.quote(300);
System.out.println("超级会员商品的最终价格为:" + quote.doubleValue());
}
}

以上,就是一个例子,可以在代码中new出不同的会员的策略类,然后执行对应的计算价格的方法。这个例子以及策略模式的相关知识,读者可以在《如何给女朋友解释什么是策略模式?》一文中学习。

但是,真正在代码中使用,比如在一个web项目中使用,上面这个Demo根本没办法直接用。

首先,在web项目中,上面我们创建出来的这些策略类都是被Spring托管的,我们不会自己去new一个实例出来。

其次,在web项目中,如果真要计算价格,也是要事先知道用户的会员等级,比如从数据库中查出会员等级,然后根据等级获取不同的策略类执行计算价格方法。

那么,web项目中真正的计算价格的话,伪代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码/**
* @author mhcoding
*/
public BigDecimal calPrice(BigDecimal orderPrice,User user) {

String vipType = user.getVipType();

if (vipType == 专属会员) {
//伪代码:从Spring中获取超级会员的策略对象
UserPayService strategy = Spring.getBean(ParticularlyVipPayService.class);
return strategy.quote(orderPrice);
}

if (vipType == 超级会员) {
UserPayService strategy = Spring.getBean(SuperVipPayService.class);
return strategy.quote(orderPrice);
}

if (vipType == 普通会员) {
UserPayService strategy = Spring.getBean(VipPayService.class);
return strategy.quote(orderPrice);
}
return 原价;
}

通过以上代码,我们发现,代码可维护性和可读性好像是好了一些,但是好像并没有减少if-else啊。

其实,在之前的《如何给女朋友解释什么是策略模式?》一文中,我们介绍了很多策略模式的优点。但是,策略模式的使用上,还是有一个比较大的缺点的:

客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。

也就是说,虽然在计算价格的时候没有if-else了,但是选择具体的策略的时候还是不可避免的还是要有一些if-else。

另外,上面的伪代码中,从Spring中获取会员的策略对象我们是伪代码实现的,那么代码到底该如何获取对应的Bean呢?

接下来我们看如何借助Spring和工厂模式,解决上面这些问题。

工厂模式为了方便我们从Spring中获取UserPayService的各个策略类,我们创建一个工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码/**
* @author mhcoding
*/
public class UserPayServiceStrategyFactory {

private static Map<String,UserPayService> services = new ConcurrentHashMap<String,UserPayService>();

public static UserPayService getByUserType(String type){
return services.get(type);
}

public static void register(String userType,UserPayService userPayService){
Assert.notNull(userType,"userType can't be null");
services.put(userType,userPayService);
}
}

这个UserPayServiceStrategyFactory中定义了一个Map,用来保存所有的策略类的实例,并提供一个getByUserType方法,可以根据类型直接获取对应的类的实例。还有一个register方法,这个后面再讲。

有了这个工厂类之后,计算价格的代码即可得到大大的优化:

1
2
3
4
5
6
7
8
9
复制代码/**
* @author mhcoding
*/
public BigDecimal calPrice(BigDecimal orderPrice,User user) {

String vipType = user.getVipType();
UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(vipType);
return strategy.quote(orderPrice);
}

以上代码中,不再需要if-else了,拿到用户的vip类型之后,直接通过工厂的getByUserType方法直接调用就可以了。

通过策略+工厂,我们的代码很大程度的优化了,大大提升了可读性和可维护性。

但是,上面还遗留了一个问题,那就是UserPayServiceStrategyFactory中用来保存所有的策略类的实例的Map是如何被初始化的?各个策略的实例对象如何塞进去的呢?

Spring Bean的注册还记得我们前面定义的UserPayServiceStrategyFactory中提供了的register方法吗?他就是用来注册策略服务的。

接下来,我们就想办法调用register方法,把Spring通过IOC创建出来的Bean注册进去就行了。

这种需求,可以借用Spring种提供的InitializingBean接口,这个接口为Bean提供了属性初始化后的处理方法,它只包括afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。

那么,我们将前面的各个策略类稍作改造即可:

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
复制代码/**
* @author mhcoding
*/
@Service
public class ParticularlyVipPayService implements UserPayService,InitializingBean {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if (消费金额大于30元) {
return 7折价格;
}
}

@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("ParticularlyVip",this);
}
}

@Service
public class SuperVipPayService implements UserPayService ,InitializingBean{

@Override
public BigDecimal quote(BigDecimal orderPrice) {
return 8折价格;
}

@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("SuperVip",this);
}
}

@Service
public class VipPayService implements UserPayService,InitializingBean {

@Override
public BigDecimal quote(BigDecimal orderPrice) {
if(该用户超级会员刚过期并且尚未使用过临时折扣){
临时折扣使用次数更新();
returen 8折价格;
}
return 9折价格;
}

@Override
public void afterPropertiesSet() throws Exception {
UserPayServiceStrategyFactory.register("Vip",this);
}
}

只需要每一个策略服务的实现类都实现InitializingBean接口,并实现其afterPropertiesSet方法,在这个方法中调用UserPayServiceStrategyFactory.register即可。

这样,在Spring初始化的时候,当创建VipPayService、SuperVipPayService和ParticularlyVipPayService的时候,会在Bean的属性初始化之后,把这个Bean注册到UserPayServiceStrategyFactory中。

以上代码,其实还是有一些重复代码的,这里面还可以引入模板方法模式进一步精简,这里就不展开了。

还有就是,UserPayServiceStrategyFactory.register调用的时候,第一个参数需要传一个字符串,这里的话其实也可以优化掉。比如使用枚举,或者在每个策略类中自定义一个getUserType方法,各自实现即可。

总结本文,我们通过策略模式、工厂模式以及Spring的InitializingBean,提升了代码的可读性以及可维护性,彻底消灭了一坨if-else。

文中的这种做法,大家可以立刻尝试起来,这种实践,是我们日常开发中经常用到的,而且还有很多衍生的用法,也都非常好用。有机会后面再介绍。

其实,如果读者们对策略模式和工厂模式了解的话,文中使用的并不是严格意义上面的策略模式和工厂模式。

首先,策略模式中重要的Context角色在这里面是没有的,没有Context,也就没有用到组合的方式,而是使用工厂代替了。

另外,这里面的UserPayServiceStrategyFactory其实只是维护了一个Map,并提供了register和get方法而已,而工厂模式其实是帮忙创建对象的,这里并没有用到。

所以,读者不必纠结于到底是不是真的用了策略模式和工厂模式。而且,这里面也再扩展一句,所谓的GOF 23种设计模式,无论从哪本书或者哪个博客看,都是简单的代码示例,但是我们日常开发很多都是基于Spring等框架的,根本没办法直接用的。

所以,对于设计模式的学习,重要的是学习其思想,而不是代码实现!!!

如果读者们感兴趣,后续可以出更多的设计模式和Spring等框架结合使用的最佳实践。希望通过这样的文章,读者可以真正的在代码中使用上设计模式。

本文转载自: 掘金

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

SpringAop源码分析(基于注解)二:筛选通知器

发表于 2019-10-21

在上篇文章SpringAop源码分析(基于注解)一中,我们分析了Spring是怎样把专门处理AOP的类进行注册的,本篇文章我们将分析这个类是怎么对AOP起作用的。

一、入口

我们已经知道BeanPostProcessors是在Bean实例化前后起作用的,如果看过前面的文章Spring Ioc源码分析 之 Bean的加载(八):初始化,应该知道Spring是在AbstractAutowireCapableBeanFactory#doCreateBean() 方法中有一个初始化Bean的方法:

1
复制代码exposedObject = initializeBean(beanName, exposedObject, mbd)

继续深入:

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
复制代码protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
//JDK的安全机制验证权限
if (System.getSecurityManager() != null) {
// <1> 激活 Aware 方法,对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
// <1> 激活 Aware 方法,对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
// <2> 后置处理器,before
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

// <3> 激活用户自定义的 init 方法
try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
// <2> 后置处理器,after
if (mbd == null || !mbd.isSynthetic()) {
// 我们关注的重点是这里!!!
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

其中第<2>步就是触发我们BeanPostProcessors的地方。

我们再回过头来看AnnotationAwareAspectJAutoProxyCreator有一个上层父类AbstractAutoProxyCreator,它实现了SmartInstantiationAwareBeanPostProcessor接口,来看下它的主要方法。

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
复制代码//AbstractAutoProxyCreator.java

//在Bean初始化之前回调
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}

/**
* Create a proxy with the configured interceptors if the bean is
* identified as one to proxy by the subclass.
* @see #getAdvicesAndAdvisorsForBean
*/
//在Bean初始化之后回调
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
//判断缓存中是否有
if (!this.earlyProxyReferences.contains(cacheKey)) {
// 没有,为 bean 生成代理对象
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

可以看到AbstractAutoProxyCreator类里实现了postProcessAfterInitialization()方法,该方法将在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
30
31
32
33
34
复制代码//AbstractAutoProxyCreator.java

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}

/*
* 如果是基础设施类(Pointcut、Advice、Advisor 等接口的实现类),或是应该跳过的类,
* 则不应该生成代理,此时直接返回 bean
*/
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// Create proxy if we have advice.
// 返回匹配当前 bean 的所有的通知器 advisor、advice、interceptor
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 核心!创建代理对象
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;
}

这里看起来逻辑不复杂:

  • 找到匹配该Bean的所有通知器
  • 创建代理对象

但是这两步具体细节就很复杂了,我们一个一个来看,先看第一步。

二、找到匹配该Bean的所有通知器

1
2
3
4
5
6
7
8
9
10
复制代码//AbstractAdvisorAutoProxyCreator

protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
//获取匹配的通知器
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
12
13
14
15
16
17
复制代码//AbstractAdvisorAutoProxyCreator.java

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
//获取所有的通知器
List<Advisor> candidateAdvisors = findCandidateAdvisors();

//筛选可应用在 beanClass 上的 Advisor,通过 ClassFilter 和 MethodMatcher
//对目标类和方法进行匹配
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
//
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
//设置排序,方便后面拦截器链执行
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

2.1、获取有所通知器

接上面的代码:

1
2
3
4
5
6
7
8
9
10
11
复制代码//AnnotationAwareAspectJAutoProxyCreator.java

protected List<Advisor> findCandidateAdvisors() {
// 调用父类方法从容器中获取所有的通知器
List<Advisor> advisors = super.findCandidateAdvisors();
// 解析 @Aspect 注解,并构建通知器
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}

2.1.1、调用父类方法从容器中获取所有的通知器

先看一下调用父类的方法

1
2
3
4
5
6
复制代码//AbstractAdvisorAutoProxyCreator.java

protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
return this.advisorRetrievalHelper.findAdvisorBeans();
}

继续深入:

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
复制代码//BeanFactoryAdvisorRetrievalHelper.java

public List<Advisor> findAdvisorBeans() {
String[] advisorNames = null;
synchronized (this) {
// cachedAdvisorBeanNames 是 advisor 名称的缓存
advisorNames = this.cachedAdvisorBeanNames;

//如果缓存为空,到容器中查找,
//并设置缓存,后续直接使用缓存即可
if (advisorNames == null) {
// 从容器中查找 Advisor 类型 bean 的名称
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
// 设置缓存
this.cachedAdvisorBeanNames = advisorNames;
}
}
if (advisorNames.length == 0) {
return new LinkedList<Advisor>();
}

List<Advisor> advisors = new LinkedList<Advisor>();
// 遍历 advisorNames
for (String name : advisorNames) {
if (isEligibleBean(name)) {
// 忽略正在创建中的 advisor bean
if (this.beanFactory.isCurrentlyInCreation(name)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping currently created advisor '" + name + "'");
}
}
else {
try {

//调用 getBean 方法从容器中获取名称为 name 的 bean,
//并将 bean 添加到 advisors 中
advisors.add(this.beanFactory.getBean(name, Advisor.class));
}
catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause();
if (rootCause instanceof BeanCurrentlyInCreationException) {
BeanCreationException bce = (BeanCreationException) rootCause;
if (this.beanFactory.isCurrentlyInCreation(bce.getBeanName())) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping advisor '" + name +
"' with dependency on currently created bean: " + ex.getMessage());
}
continue;
}
}
throw ex;
}
}
}
}

return advisors;
}

这段代码虽然很长,但并不复杂:

  • 先从缓存中获取,获取不到就从IOC容器中获取类型为Advisor的BeanName
  • 遍历获取到的BeanName,调用getBean()方法获取实例,并加入到通知器集合中

2.1.2、解析 @Aspect 注解,并构建通知器

代码如下:

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
复制代码//BeanFactoryAspectJAdvisorsBuilder.java

public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;

if (aspectNames == null) {
synchronized (this) {
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new LinkedList<>();
aspectNames = new LinkedList<>();
// 从容器中获取所有 bean 的名称
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
if (!isEligibleBean(beanName)) {
continue;
}
// We must be careful not to instantiate beans eagerly as in this case they
// would be cached by the Spring container but would not have been weaved.
// 根据 beanName 获取 bean 的类型
Class<?> beanType = this.beanFactory.getType(beanName);
if (beanType == null) {
continue;
}
// 检测 beanType 是否包含 Aspect 注解
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
//创建Aspect元数据
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
//创建元数据aop实例化工厂
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
// 从工厂中获取通知器
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new LinkedList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

代码很长,但我们在只需要关注关键步骤即可:

  • 从容器中获取所有 bean 的名称
  • 遍历,根据 beanName 获取 bean 的类型
  • 检测 beanType 是否包含 Aspect 注解
  • 从工厂中获取通知器

这里也可以和我们前面的demo对应起来,我们之前定义了一个LogAspect的类,然后用注解@Component和@Aspect声明了。

上面这段代码的逻辑就是:找到这个标注@Aspect的类,并找到里面定义的通知器,如@Before、@After等。

同时这也回答了上篇文章的一个问题:Spring是怎么找到我们定义的切面的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Aspect
@Component
@EnableAspectJAutoProxy
public class LogAspect {

@Before("execution(* com.mydemo.work.StudentController.getName(..))")
public void doBefore() {
System.out.println("========before");
}

@After("execution(* com.mydemo.work.StudentController.getName(..))")
public void doAfter() {
System.out.println("========after");
}
}

接着看从工厂获取通知器的方法this.advisorFactory.getAdvisors(factory)

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
复制代码//ReflectiveAspectJAdvisorFactory.java

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
validate(aspectClass);

// We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
// so that it will only instantiate once.
MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

List<Advisor> advisors = new LinkedList<>();
<1> //获取该切面的所有方法,排除@Pointcut修饰的
for (Method method : getAdvisorMethods(aspectClass)) {
//遍历,获取被 通知注解 修饰的方法,并封装成Advisor
<2> Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
if (advisor != null) {
advisors.add(advisor);
}
}

// If it's a per target aspect, emit the dummy instantiating aspect.
if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
advisors.add(0, instantiationAdvisor);
}

// Find introduction fields.
for (Field field : aspectClass.getDeclaredFields()) {
Advisor advisor = getDeclareParentsAdvisor(field);
if (advisor != null) {
advisors.add(advisor);
}
}

return advisors;
}

接着追踪getAdvisor()方法:

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
复制代码public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
int declarationOrderInAspect, String aspectName) {

validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
//获取切点Pointcut
<3> AspectJExpressionPointcut expressionPointcut = getPointcut(
candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
if (expressionPointcut == null) {
return null;
}

// 创建 Advisor 实现类,封装切点表达式、通知名称、方法名称等
<6> return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

@Nullable
private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
// 获取方法上的 AspectJ 相关注解,包括 @Before,@After、@Around、@Pointcut 等
//因为这些注解上都可以设置切点
<4> AspectJAnnotation<?> aspectJAnnotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
if (aspectJAnnotation == null) {
return null;
}

AspectJExpressionPointcut ajexp =
new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
<5> //设置切点匹配表达式
ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
if (this.beanFactory != null) {
ajexp.setBeanFactory(this.beanFactory);
}
return ajexp;
}

这里的逻辑其实也不复杂,。

  • <1>,获取切面中的所有方法,排除@Pointcut修饰的方法
  • <2>,遍历所有方法
  • <3>,获取该方法的切点
  • <4>,根据AspectJ相关注解,包括 @Before,@After、@Pointcut等获取切点
  • <5>,设置切点表达式到AspectJExpressionPointcut
    封装结果如下:

  • <6>,创建Advisor,封装切点表达式、通知名称、方法名称等

封装结果如下:

this.advisorFactory.getAdvisors(factory)这段代码的最终目的,就是获取该切面所有的通知方法、它们的切点,并把它们都封装成一个个Advisor。

但其实每个Advisor里的Advice都是不同的,我们来看下创建Advisor的过程,即第<6>步:

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
复制代码//InstantiationModelAwarePointcutAdvisorImpl.java

public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

this.declaredPointcut = declaredPointcut;
this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
this.methodName = aspectJAdviceMethod.getName();
this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
this.aspectJAdviceMethod = aspectJAdviceMethod;
this.aspectJAdvisorFactory = aspectJAdvisorFactory;
this.aspectInstanceFactory = aspectInstanceFactory;
this.declarationOrder = declarationOrder;
this.aspectName = aspectName;

if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
// Static part of the pointcut is a lazy type.
Pointcut preInstantiationPointcut = Pointcuts.union(
aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

// Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
// If it's not a dynamic pointcut, it may be optimized out
// by the Spring AOP infrastructure after the first evaluation.
this.pointcut = new PerTargetInstantiationModelPointcut(
this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
this.lazy = true;
}
else {
// A singleton aspect.
this.pointcut = this.declaredPointcut;
this.lazy = false;
// 按照注解解析 Advice
this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
}
}

上面是 InstantiationModelAwarePointcutAdvisorImpl 的构造方法,不过我们无需太关心这个方法中的一些初始化逻辑。我们把目光移到构造方法的最后一行代码中,即 instantiateAdvice(this.declaredPointcut),这个方法用于创建通知 Advice。

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
复制代码private Advice instantiateAdvice(AspectJExpressionPointcut pcut) {
return this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pcut,
this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
}

public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
validate(candidateAspectClass);

// 获取 Advice 注解
AspectJAnnotation<?> aspectJAnnotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
if (aspectJAnnotation == null) {
return null;
}

if (!isAspect(candidateAspectClass)) {
throw new AopConfigException("Advice must be declared inside an aspect type: " +
"Offending method '" + candidateAdviceMethod + "' in class [" +
candidateAspectClass.getName() + "]");
}

if (logger.isDebugEnabled()) {
logger.debug("Found AspectJ method: " + candidateAdviceMethod);
}

AbstractAspectJAdvice springAdvice;

// 按照注解类型生成相应的 Advice 实现类
switch (aspectJAnnotation.getAnnotationType()) {
case AtBefore: // @Before -> AspectJMethodBeforeAdvice
springAdvice = new AspectJMethodBeforeAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;

case AtAfter: // @After -> AspectJAfterAdvice
springAdvice = new AspectJAfterAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;

case AtAfterReturning: // @AfterReturning -> AspectJAfterAdvice
springAdvice = new AspectJAfterReturningAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
if (StringUtils.hasText(afterReturningAnnotation.returning())) {
springAdvice.setReturningName(afterReturningAnnotation.returning());
}
break;

case AtAfterThrowing: // @AfterThrowing -> AspectJAfterThrowingAdvice
springAdvice = new AspectJAfterThrowingAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
}
break;

case AtAround: // @Around -> AspectJAroundAdvice
springAdvice = new AspectJAroundAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;


//什么都不做,直接返回 null。
case AtPointcut:
if (logger.isDebugEnabled()) {
logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
}
return null;

default:
throw new UnsupportedOperationException(
"Unsupported advice type on method: " + candidateAdviceMethod);
}

springAdvice.setAspectName(aspectName);
springAdvice.setDeclarationOrder(declarationOrder);

//获取方法的参数列表名称,比如方法 int sum(int numX, int numY),
//getParameterNames(sum) 得到 argNames = [numX, numY]
String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
if (argNames != null) {
// 设置参数名
springAdvice.setArgumentNamesFromStringArray(argNames);
}
springAdvice.calculateArgumentBindings();
return springAdvice;
}

可见,根据注解的不同,创建不同的Advice,并封装到Advisor中。

2.2、筛选合适的通知器

现在我们已经拿到了所有通知器,接下来就要筛选出匹配当前Bean的通知器。
代码List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码//AbstractAdvisorAutoProxyCreator.java

protected List<Advisor> findAdvisorsThatCanApply(
List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

ProxyCreationContext.setCurrentProxiedBeanName(beanName);
try {
//筛选匹配的通知器
return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
}
finally {
ProxyCreationContext.setCurrentProxiedBeanName(null);
}
}

继续深入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码//AbstractAdvisorAutoProxyCreator.java

protected List<Advisor> findAdvisorsThatCanApply(
List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

ProxyCreationContext.setCurrentProxiedBeanName(beanName);
try {
//筛选出匹配当前Bean的通知器
return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
}
finally {
ProxyCreationContext.setCurrentProxiedBeanName(null);
}
}
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
复制代码//AopUtils.java

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
if (candidateAdvisors.isEmpty()) {
return candidateAdvisors;
}
List<Advisor> eligibleAdvisors = new LinkedList<>();
// 筛选 IntroductionAdvisor 类型的通知器
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码//AopUtils.java

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
//ClassFilter直接匹配
if (advisor instanceof IntroductionAdvisor) {
return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
}
else if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pca = (PointcutAdvisor) advisor;
//继续调用重载方法
return canApply(pca.getPointcut(), targetClass, hasIntroductions);
}
else {
// It doesn't have a pointcut so we assume it applies.
return true;
}
}
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
复制代码//AopUtils.java

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}

MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
// No need to iterate the methods if we're matching any method anyway...
return true;
}

IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
}

//查找当前类及其父类(以及父类的父类等等)所实现的接口,由于接口中的方法是 public,
//所以当前类可以继承其父类,和父类的父类中所有的接口方法
Set<Class<?>> classes = new LinkedHashSet<>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
classes.add(targetClass);
for (Class<?> clazz : classes) {
// 获取当前类的方法列表,包括从父类中继承的方法
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
// 使用 methodMatcher 匹配方法,匹配成功即可立即返回
if ((introductionAwareMethodMatcher != null &&
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}

return false;
}

上面就是筛选通知器的过程,筛选的工作主要由 ClassFilter 和 MethodMatcher 来完成。关于 ClassFilter 和 MethodMatcher,在 AOP 中,切点 Pointcut 是用来匹配连接点的,以 AspectJExpressionPointcut 类型的切点为例。该类型切点实现了ClassFilter 和 MethodMatcher 接口,匹配的工作则是由 AspectJ 表达式解析器负责。除了使用 AspectJ 表达式进行匹配,Spring 还提供了基于正则表达式的切点类,以及更简单的根据方法名进行匹配的切点类。大家有兴趣的话,可以自己去了解一下,这里就不多说了。

现在,我们知道了通知是怎么创建和筛选的。那下篇文章,我们一起来分析一下AOP是怎么创建代理对象的。

总结

这篇文章花了比较大的功夫,受个人能力限制,很遗憾没有对里面的源码作非常详细的分析,只理解了主流程,希望朋友们发现文章中的错误或不妥之处,还请指出,互相交流~

参考:

www.tianxiaobo.com/2018/06/20/…

本文转载自: 掘金

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

面试官 谈谈你对Java反射的理解

发表于 2019-10-20

Java提供的反射机制允许你在运行时检查类的信息

Java的类加载

Java在真正需要使用一个类时才会去加载类,而不是在启动程序时就载入所有的类,因为大多数使用者都只使用到程序的部分资源,在需要某些功能时再载入某些资源,可以让系统资源运用的更高效。

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在Jvm的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

Java 中的所有类型包括基本类型(int, long, float等等),即使是数组都有与之关联的 Class 类的对象。
Class对象是由Jvm自动生成的,每当一个类被载入时,Jvm就自动为其生成一个Class对象

Class对象

实例.getClass()

通过Object的getClass()获取每一个实例对应的Class对象

1
2
3
4
5
6
7
java复制代码String name = "hello";  
Class stringClass = name.getClass();
System.out.println("类的名称:" + stringClass.getName());
System.out.println("是否为接口:" + stringClass.isInterface());
System.out.println("是否为基本类型:" + stringClass.isPrimitive());
System.out.println("是否为数组:" + stringClass.isArray());
System.out.println("父类名称:" + stringClass.getSuperclass().getName());
1
2
3
4
5
复制代码类的名称:java.lang.String  
是否为接口:false
是否为基本类型:false
是否为数组:false
父类名称:java.lang.Object

类名.class

你也可以直接使用一下方式来获取String类的Class对象

1
java复制代码Class stringClass = String.class;

Class.forName()

在一些应用中,你无法事先知道使用者将载入什么类别,你可以使用Class的静态方法forName()来动态加载类别

1
2
3
4
5
6
java复制代码Class c = Class.forName(args[0]);  
System.out.println("类的名称:" + c.getName());
System.out.println("是否为接口:" + c.isInterface());
System.out.println("是否为基本类型:" + c.isPrimitive());
System.out.println("是否为数组:" + c.isArray());
System.out.println("父类名称:" + c.getSuperclass().getName());
1
2
3
4
5
6
复制代码$ java Demo1 java.util.Scanner    
类的名称:java.util.Scanner
是否为接口:false
是否为基本类型:false
是否为数组:false
父类名称:java.lang.Object

Class.forName()有两个版本,上面的版本只指定了全限定类名,而另一个版本可以让你指定类名,载入时是否执行静态代码块,执行类加载器(ClassLoader)

1
java复制代码static Class forName(String name, boolean initialize, ClassLoader loader)
1
2
3
4
5
6
7
8
9
10
11
java复制代码ClassLoader loader = Thread.currentThread().getContextClassLoader();  
// Class.forName() 加载类 默认会执行初始化块
Class.forName("Test2");
// Class.forName() 加载类 第二个参数 可以控制是否执行初始化块
Class.forName("Test2", false, loader);

class Test2 {
    static {
        System.out.println("静态初始化块执行了!");
    }
}

从Class对象中获取信息

Class对象表示所载入的类别,获取Class对象后,你就可以获取类别相关的信息,入 package, constructor, field, method等信息。
而每一种信息,都有相对应的类别

  • package: java.lang.reflect.Package
  • constructor: java.lang.reflect.Constructor
  • field: java.lang.reflect.Field
  • method: java.lang.reflect.Method
1
2
3
4
5
6
7
java复制代码Class c = Class.forName(args[0]);  
System.out.println("包信息package:" + c.getPackage());
System.out.println("类修饰符modifier:" + c.getModifiers());
System.out.println("构造方法constructor:");
Arrays.stream(c.getDeclaredConstructors()).forEach(System.out::println);
System.out.println("成员变量fields:");
Arrays.stream(c.getDeclaredFields()).forEach(System.out::println);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码$ java Demo1 java.util.ArrayList  
包信息package:package java.util
类修饰符modifier:1
构造方法constructor:
public java.util.ArrayList(java.util.Collection)
public java.util.ArrayList()
public java.util.ArrayList(int)
成员变量fields:
private static final long java.util.ArrayList.serialVersionUID
private static final int java.util.ArrayList.DEFAULT_CAPACITY
private static final java.lang.Object[] java.util.ArrayList.EMPTY_ELEMENTDATA
private static final java.lang.Object[] java.util.ArrayList.DEFAULTCAPACITY_EMPTY_ELEMENTDATA
transient java.lang.Object[] java.util.ArrayList.elementData
private int java.util.ArrayList.size
private static final int java.util.ArrayList.MAX_ARRAY_SIZE

ClassLoader 类加载器

Java在需要使用类的时候才会将类载入,Java中类的载入是由Class Loader来实现的.

当你尝试执行java xxx命令时,java会尝试找到JRE的安装目录,然后寻找
jvm.dll,接着启动JVM并进行初始化操作,接着产生
BootstrapLoader,Bootstrap Loader会载入Extended Loader, 并设定Extended Loader 的parent 为 BootstrapLoader, 接着Bootstrap Loader 会载入 Application Loader, 并将Application Loader 的parent 设定为
Extended Loader

启动类加载器

BootstrapLoader搜寻 sun.boot.library.path中指定的类, 你可以使用
System.getProperty("sun.boot.library.path")来获取

1
复制代码/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib

扩展类加载器

Extended Loader(sun.misc.Launcher$ExtClassLoader) 是由Java编写的,会搜寻系统参数java.ext.dirs中指定的类别,可以通过System.getProperty("java.ext.dirs")来获取

1
2
3
4
5
复制代码/Users/dsying/Library/Java/Extensions:  
/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:
/Network/Library/Java/Extensions:
/System/Library/Java/Extensions:/usr/lib/java

应用程序类加载器

Application Loader
(sun.misc.Launcher$AppClassLoader) 是由Java编写的,会搜寻系统参数java.class.path中指定的类别,可以通过System.getProperty("java.class.path")来获取, 在使用java xxx命令执行.class字节码文件时,可以通过-cp参数设定classpath

1
shell复制代码java –cp ./classes SomeClass

类加载器之间的关系

1
2
3
4
5
6
7
java复制代码ClassLoader loader Thread.currentThread().getContextClassLoader();  
// sun.misc.Launcher$AppClassLoader@18b4aac2   应用类加载器
System.out.println(loader);
// sun.misc.Launcher$ExtClassLoader@610455d6   扩展类加载器
System.out.println(loader.getParent());
// Bootstrap ClassLoader 启动类加载器(用C语言实现,所以此处返回null)
System.out.println(loader.getParent().getParent());
1
2
3
复制代码sun.misc.Launcher$AppClassLoader@18b4aac2  
sun.misc.Launcher$ExtClassLoader@610455d6
null

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载
  2. 通过Class.forName()方法动态加载
  3. 通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()区别
  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

ClassLoader源码分析:

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复制代码public Class<?> loadClass(String name)throws ClassNotFoundException {  
    return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
    // 首先判断该类型是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
        try {
            if (parent != null) {
                //如果存在父类加载器,就委派给父类加载器加载
                c = parent.loadClass(name, false);
            } else {
                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
                //如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                c = findClass(name);
            }
        }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

自定义加载器

自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

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
java复制代码package com.github.hcsp.classloader;  

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
    // 存放字节码文件的目录
    private final File bytecodeFileDirectory;

    public MyClassLoader(File bytecodeFileDirectory) {
        this.bytecodeFileDirectory = bytecodeFileDirectory;
    }

    // 还记得类加载器是做什么的么?
    // "从外部系统中,加载一个类的定义(即Class对象)"
    // 请实现一个自定义的类加载器,将当前目录中的字节码文件加载成为Class对象
    // 提示,一般来说,要实现自定义的类加载器,你需要覆盖以下方法,完成:
    //
    // 1.如果类名对应的字节码文件存在,则将它读取成为字节数组
    //   1.1 调用ClassLoader.defineClass()方法将字节数组转化为Class对象
    // 2.如果类名对应的字节码文件不存在,则抛出ClassNotFoundException
    //
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getByteArrayFromFile(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    byte[] getByteArrayFromFile(String className) throws ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        File file = new File(bytecodeFileDirectory, className + ".class");
        int len = 0;
        try {
            byte[] bufferSize = new byte[1024];
            FileInputStream fis = new FileInputStream(file);
            while ((len = fis.read(bufferSize)) != -1) {
                bos.write(bufferSize, 0, len);
            }
        } catch (FileNotFoundException e) {
            throw new ClassNotFoundException();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        File projectRoot = new File(System.getProperty("basedir", System.getProperty("user.dir")));
        MyClassLoader myClassLoader = new MyClassLoader(projectRoot);

        Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
        Object testClassInstance = testClass.getConstructor().newInstance();
        String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
        System.out.println(message);
    }
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程.

其它

使用反射创建对象

你可以使用Class的newInstance()方法来实例化

1
2
java复制代码Class c = Class.forName(className);  
Object obj = c.newInstance();

调用方法

使用反射可以取回类中的方法,方法对应的类为
java.lang.reflect.Method, 你可以使用它的
invoke()方法来调用指定的方法

1
2
3
java复制代码Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");  
Object testClassInstance = testClass.getConstructor().newInstance();
String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);

本文转载自: 掘金

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

1…852853854…956

开发者博客

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