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

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


  • 首页

  • 归档

  • 搜索

从0开始学大数据-数据仓库理论篇

发表于 2019-07-22

什么是数据仓库

数据仓库(DW)是一个 面向主题的、集成的、稳定的、随时间变化的数据的集合,以用于支持管理决策过程。

建立数据仓库的目的是为企业高层系统地组织、理解和使用数据以便进行战略决策。

数据仓库的特征

数据仓库有以下几大特征:

(1)面向主题

主题是指用户使用数据仓库进行决策时所关心的重点领域。数据仓库通过一个个主题将多个业务系统的数据加载到一起,为了各个主题(如:用户、订单、商品等)进行分析而建,操作型数据库是为了支撑各种业务而建立。

(2)集成性

数据仓库会将不同源数据库中的数据汇总到一起,但是并不是简单的复制,而是经过了抽取、筛选、清理、转换、综合等工作。

(3)稳定性即非易失的

数据仓库的数据是为了企业数据分析而建立,所以数据被加载后一般会保存较长时间。数据仓库中的数据大多表示过去某一时刻的数据,主要用于查询、分析,不会经常进行修改、添加等操作。

(4)随时间而变化即时变的

数据仓库存储的是历史数据,它会定期从操作型应用系统中接收新的数据。所以数据仓库中的数据一般都有个时间维度。数据仓库实际是记录了系统的各个瞬时,并通过瞬态连接起来形成动画(即数据仓库的快照集合),从而在数据分析时再现系统运动的全过程。

为什么使用数据仓库

通常数据仓库的数据是来自各个业务应用系统,然后业务系统中的数据形式是多种多样的,可能是Oracle、MySQL、SQL Server 等关系数据库里的结构化数据,也有可能是文本、CSV 等平面文件或Word、Excel 文档中的非结构化数据,还有可能是 HTML、XML 等自描述的半结构化数据。这些数据经过一系列的 数据抽取、转换、清洗,最终以一种统一的格式装载进数据仓库。数据仓库里的数据作为分析用的数据源,提供给后面的 即系查询、分析系统、数据集市、报表系统、数据挖掘系统等。

使用数据仓库有以下好处:

  • 将多个数据源集成到单一数据存储,因此可以使用单一数据查询引擎展示数据。
  • 缓解在事务处理数据库上因执行大查询而产生的资源竞争问题。
  • 维护历史数据。
  • 通过对多个源系统的数据整合,使得在整个企业的角度存在同一的中心视图。
  • 通过提供一致的编码和描述,减少或修正坏数据问题,提高数据质量
  • 一致性地表示组织信息。
  • 提供所有数据的单一通用数据模型,而不用关心数据源。
  • 重构数据,使数据对业务更有意义。
  • 向复杂分析查询交付优秀的查询性能,同时不影响操作型系统。
  • 开发决策性查询更简单。

数据仓库与传统数据库的区别

数据仓库虽然是从传统数据库系统发展而来,但是两者还是存在着诸多差异。

数据仓库系统组成

数据仓库系统以数据仓库为核心,将各种应用系统集成在一起,为统一的历史数据分析提供坚实的平台,通过数据分析与报表模块的查询和分析工具 OLAP (联机分析处理)、决策分析、数据挖掘完成对信息的提取,以满足决策的需要。

整个数据仓库系统分为:源数据层、数据存储和管理层、OLAP 服务器层、前端分析工具层。

数据仓库系统体系结构

数据仓库系统各组成部分:

  • 数据仓库:数据仓库是整个数据仓库环境的核心,是数据存放的地方,提供了对数据检索的支持。支持海量数据的存储和快速检索。
  • 抽取工具:抽取工具将数据从各种数据源中提取出来,进行清洗转化,再存放到数据仓库中。
  • 元数据:元数据是描述数据仓库内数据的结构、位置和建立方法的数据。通过元数据进行数据仓库的管理和通过元数据来使用数据仓库。
  • 数据集市:数据集市是完整的数据仓库的一个子集,为了特定的应用目的或应用范围,而从数据仓库中独立出来的一部分数据,也可以称为部门数据或主题数据。其目的是减少数据处理量,使信息的利用更加快捷和灵活。
  • OLAP 服务:提供对存储在数据仓库中数据分析能力,能够快速进行复杂数据查询和聚集,并帮助用户分析多维数据中的各维情况。
  • 前端工具:主要包括各种报表工具、查询工具、数据分析工具、数据挖掘工具以及各种基于数据仓库或数据集市的应用开发工具。

ETL

ETL 用来描述 数据抽取、清洗转换 和 加载 的过程。ETL按照统一的规则集成并提高数据的质量,是将数据从数据源向目标数据仓库(DW)转化的过程。ETL 是商务智能/数据仓库的核心和灵魂。

1. 数据抽取

数据抽取是从各个不同的数据源抽取数据并存储到操作数据存储(Operational Data Store,ODS)中的过程。

2. 数据清洗转换

数据清洗转是指按照预先设计好的规则将抽取的数据进行转换,使本来异构的数据格式能统一起来。

数据清洗转换包括 数据清洗 和 数据转换 两个过程。

  • 数据清洗 是指对空数据、缺失数据进行补缺操作,对非法数据进行替换,保证数据的正确性。
  • 数据转换 是指对数据进行整合、拆分和变换。
+ **数据整合**是指通过多表关联,将不同类型数据之间可能存在潜在关联关系的多条数据进行合并,通过数据的整合,丰富数据维度,有利于发现更多有价值的信息。
+ **数据拆分**是指按一定规则对数据进行拆分,将一条数据拆分为多条。
+ **数据变换**是指对数据进行行列转换、排序、修改序号、去除重复记录变换操作。

3. 数据装载

数据装载是指将清洗转换完的数据加载到数据仓库中。数据加载的方式主要有:

  • 增量加载
    • 时间戳方式
    • 日志表方式
    • 全表对比方式
  • 全量加载
    • 全表删除再插入方式

数据集市

数据集市(Data Mart)是完整的数据仓库的一个子集,为了特定的应用目的或应用范围,而从数据仓库中独立出来的一部分数据,也可以成为部门数据或主题数据。而数据仓库正式由其所有的数据集市有机组合而成的的。且各数据集市间应协调一致,满足整个企业分析决策的需要。

建立数据集市与数据仓库,一般是采用 “自顶向下“ 和 “自下而上” 相结合的设计思想。

数据仓库和数据集市的区别

  • 数据仓库向各个数据集市提供数据。前者是企业级的,规模较大,后者是部门级的,相对规模较小。
  • 若干个部门的数据集市组成一个数据仓库。数据集市开发周期短、速度快,数据仓库开发的周期长。速度慢。
  • 从其数据特征进行分析,数据仓库中的数据结构采用规范化模式,数据集市中的数据采用星型模式。通常数据仓库中的数据粒度比数据集市的粒度要细。

OLTP vs OLAP

OLTP

OLTP(联机事务处理) 是传统关系型数据库的重要应用之一,主要是基本的、日常的事务处理,对响应要求比较高,强条的是密集数据的更新处理的性能和系统的可靠性及效率。

OLTP 是事件驱动、面向应用的。

OLTP主要特点:

  • 对响应时间要求非常高;
  • 用户数量非常庞大,主要是操作人员;
  • 数据库的各种操作基于索引进行;
  • 对数据库的事务均已实现定义,查询简单,一般不涉及多表连接操作。

OLAP

OLAP(联机分析处理) 是一种多维分析技术,用来满足决策用户在大量的业务数据中,从多角度探索业务活动的规律性、市场的运作趋势的分析需求,并辅助他们进行战略发展决策的制定。

OLAP 系统按照数据存储方式可以分为:

  • ROLAP:将分析要用的多维数据存储在关系数据库中,并根据应用的需求有选择的定义一批视图,视图也是存储在关系数据库中。
  • MOLAP:将 OLAP 分析要用的多维数据物理上存储为多维数组的形式,形成“立方体”的结构。
  • HOLAP:把 MOLAP 和 ROLAP 两种结构的有点有机的结合起来,能满足用户各种复杂的分析需求。

OLAP 工具是针对特定问题的联机数据访问与分析,它通过多维的方式对数据进行分析、查询和报表。

多维分析是指对以多维形式组织起来的数据采取切片、切换、钻取、旋转等各种分析动作,以求剖析数据,使用户能从多个角度、多个侧面地观察数据库中的数据,从而深入理解包含在数据中的信息。

  • 钻取:改变维的层次,变换分析的粒度。包括:
    • 向上钻取:在某一维上将低层次的细节数据概括到高层次的汇总数据,或者减少维数。
    • 向下钻取:从汇总数据深入到细节数据进行观察或增加新维度。
  • 切片和切换:在一部分维上选定值后,关心度量数据在剩余维上的分布。如果剩余的维只有两个,则是切片;如果有三个,则是切换。
  • 旋转:变换维的方向,即在表格中重新安排维的放置(如:行列互换)。

OLTP 和 OLAP 对比

– END –
欢迎长按下图关注公众号DigNew


推荐阅读:

  • 从0开始学大数据-Hive性能优化篇
  • 从0开始学大数据-Hive基础篇
  • 带你快速上手HBase | HBase读写性能优化
  • 带你快速上手HBase | HBase列族优化
  • 带你快速上手HBase | HBase RowKey设计

本文转载自: 掘金

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

macos安装ffmpeg以及出现问题的解决方案,一次成功

发表于 2019-07-19

安装ffmpeg使用brew
首先安装brew:

1
复制代码/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

出现这个:==> Installation successful!安装成功

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

No changes to formulae.
==> Installation successful!

==> Homebrew has enabled anonymous aggregate formulae and cask analytics.
Read the analytics documentation (and how to opt-out) here:
https://docs.brew.sh/Analytics

==> Homebrew is run entirely by unpaid volunteers. Please consider donating:
https://github.com/Homebrew/brew#donations
==> Next steps:
- Run `brew help` to get started
- Further documentation:
https://docs.brew.sh

看看相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码$ brew
Example usage:
brew search [TEXT|/REGEX/]
brew info [FORMULA...]
brew install FORMULA...
brew update
brew upgrade [FORMULA...]
brew uninstall FORMULA...
brew list [FORMULA...]

Troubleshooting:
brew config
brew doctor
brew install --verbose --debug FORMULA

Contributing:
brew create [URL [--no-fetch]]
brew edit [FORMULA...]

Further help:
brew commands
brew help [COMMAND]
man brew
https://docs.brew.sh

安装ffmpeg,使用brew安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码$ brew install  ffmpeg 
==> Installing dependencies for ffmpeg: frei0r, libtasn1, nettle, p11-kit, openssl, libevent, unbound, gnutls, lame, fribidi, gettext, python, glib, lzo, pixman, cairo, graphite2, icu4c, harfbuzz, libass, libbluray, libsoxr, libogg, libvorbis, libvpx, opencore-amr, opus, flac, libsndfile, libsamplerate, rubberband, sdl2, snappy, speex, webp, theora, x264, x265 and xvid
==> Installing ffmpeg dependency: frei0r
==> Downloading https://homebrew.bintray.com/bottles/frei0r-1.6.1.mojave.bottle.1.tar.gz
==> Downloading from https://akamai.bintray.com/a5/a509ee11dc4a3cd431a888c708d32c53d81e5ca67250520f91284d4370d946d4?__gda__=exp=1563528408~hmac=dd80d6bc9693a885841309c5bc094e71743
######################################################################## 100.0%
==> Pouring frei0r-1.6.1.mojave.bottle.1.tar.gz
🍺 /usr/local/Cellar/frei0r/1.6.1: 137 files, 2.0MB
==> Installing ffmpeg dependency: libtasn1
==> Downloading https://homebrew.bintray.com/bottles/libtasn1-4.13.mojave.bottle.tar.gz
######################################################################## 100.0%
==> Pouring libtasn1-4.13.mojave.bottle.tar.gz
🍺 /usr/local/Cellar/libtasn1/4.13: 59 files, 436KB
==> Installing ffmpeg dependency: nettle

出现下面的表名正在下载安装ffmpeg:注意安装目录

1
2
3
4
5
6
复制代码==> Installing ffmpeg
==> Downloading https://homebrew.bintray.com/bottles/ffmpeg-4.1.4_1.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/1f/1fff696effdf5cdea3feb1b2d022018c6dfe47b71de640a46adc4631cdeccf1e?__gda__=exp=1563528747~hmac=8eb115e7fc1a65ade91bdda74c9fd572e38
######################################################################## 100.0%
==> Pouring ffmpeg-4.1.4_1.mojave.bottle.tar.gz
🍺 /usr/local/Cellar/ffmpeg/4.1.4_1: 282 files, 55.0MB

一切安装顺利,然鹅。。。。然鹅,执行ffmpeg命名报错了
出现下面的错误:

1
2
3
4
5
复制代码$ ffmpeg
dyld: Library not loaded: /usr/local/opt/freetype/lib/libfreetype.6.dylib
Referenced from: /usr/local/bin/ffmpeg
Reason: image not found
[1] 22518 abort ffmpeg

报错:

1
2
复制代码dyld: Library not loaded:
/usr/local/opt/freetype/lib/libfreetype.6.dylib

就是freetype动态库指定的目录不存在,或许没有安装,使用brew命令安装,显示已经安装,重新卸载安装也不行
但是安装完成之后也不行,一直报错
执行命令报错:

1
2
3
4
5
复制代码$ ffmpeg
dyld: Library not loaded: /usr/local/opt/freetype/lib/libfreetype.6.dylib
Referenced from: /usr/local/bin/ffmpeg
Reason: image not found
[1] 22518 abort ffmpeg

进入ffmpeg安装目录,查看是否是因为路径问题

1
2
3
4
5
6
复制代码$ cd /usr/local/Cellar/ffmpeg/4.1.4_1/bin
$ ./ffmpeg
dyld: Library not loaded: /usr/local/opt/freetype/lib/libfreetype.6.dylib
Referenced from: /usr/local/bin/ffmpeg
Reason: image not found
[1] 22518 abort ffmpeg

发现还是报错。于是还是从查看ffmepg的动态库freetype入手,看看相关的功能。

查看目录中是否有相关的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码$ cd  /usr/local/opt/freetype/lib/
cd: no such file or directory: /usr/local/opt/freetype/lib/
$ cd /usr/local/opt/
$ ls
aom fontconfig jansson libnettle libvpx node.js python tidy-html5
apr freetds jemalloc libogg libvterm node@11 python3 tree
apr-util frei0r jpeg libomp libzip nodejs python@2 unbound
argon2 fribidi lame libpng little-cms2 npm readline unibilium
aspell gdbm lcms2 libpq luajit nvim rtmpdump unixodbc
autoconf gettext leptonica libsamplerate lzo opencore-amr rubberband webp
autoconf@2.69 giflib libass libsndfile mongo openexr sdl2 wget
boost glib libbluray libsodium mongodb openjpeg shared-mime-info x264
boost@1.69 gmp libde265 libsoxr mongodb-community openldap snappy x265
bower gnutls libev libssh2 mongodb-community@4.0 openssl speex xvid
brotli graphite2 libevent libtasn mongodb@4.0 opus sqlite xz
c-ares harfbuzz libffi libtasn1 msgpack p11-kit sqlite3 yarn
cairo htop libheif libtermkey ncurses pcre tcpdump
curl htop-osx libidn libtiff neovim pcre1 telnet
curl-openssl icu4c libidn2 libtool netcat pcre2 tesseract
ffmpeg ilmbase libjpeg libunistring nettle php@7.2 theora
ffmpeg@4 imagemagick libjpg libuv nghttp2 pixman thrift
flac imagemagick@7 libmetalink libvorbis node proxychains-ng thrift@0.12

可以看到并不存在,同时发现新安装的freetype目录在其他位置,通过建立软连接解决问题。

通过先卸载后安装freetype确定安装目录

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
复制代码$ brew upgrade freetype           
Error: freetype 2.10.1 already installed

# yanglei @ yangleideMacBook-Pro in /usr/local/opt [17:33:50] C:1
$ brew uninstall freetype
Uninstalling /usr/local/Cellar/freetype/2.10.1... (61 files, 2.2MB)

# yanglei @ yangleideMacBook-Pro in /usr/local/opt [17:34:11]
$ brew install freetype
==> Downloading https://homebrew.bintray.com/bottles/freetype-2.10.1.mojave.bottle.tar.gz
Already downloaded: /Users/yanglei/Library/Caches/Homebrew/downloads/4e8dc5929a6cd94508b9908d8d04d326817a99cd66c009056b2fe3a60d182bd1--freetype-2.10.1.mojave.bottle.tar.gz
==> Pouring freetype-2.10.1.mojave.bottle.tar.gz
🍺 /usr/local/Cellar/freetype/2.10.1: 61 files, 2.2MB

# yanglei @ yangleideMacBook-Pro in /usr/local/opt [17:34:39]
$ cd /usr/local/opt/freetype/lib/libfreetype.6.dylib
cd: not a directory: /usr/local/opt/freetype/lib/libfreetype.6.dylib


cd /usr/local/Cellar/freetype/2.10.1/
$ ln -s /usr/local/Cellar/freetype/2.10.1/bin/freetype-config /usr/local/opt/freetype/lib/libfreetype.6.dylib
ln: /usr/local/opt/freetype/lib/libfreetype.6.dylib: File exists

# yanglei @ yangleideMacBook-Pro in /usr/local/Cellar/freetype/2.10.1/bin [17:36:55] C:1
$ cd /usr/local/opt/freetype/lib/

# yanglei @ yangleideMacBook-Pro in /usr/local/opt/freetype/lib [17:37:11]
$ ls
libfreetype.6.dylib libfreetype.a libfreetype.dylib pkgconfig

# yanglei @ yangleideMacBook-Pro in /usr/local/opt/freetype/lib [17:37:12]
$ l
total 2744
drwxr-xr-x 6 yanglei staff 192B Jul 1 23:55 .
drwxr-xr-x 10 yanglei staff 320B Jul 19 17:34 ..
-r--r--r-- 1 yanglei staff 591K Jul 19 17:34 libfreetype.6.dylib
-r--r--r-- 1 yanglei staff 780K Jul 1 23:55 libfreetype.a
lrwxr-xr-x 1 yanglei staff 19B Jul 1 23:55 libfreetype.dylib -> libfreetype.6.dylib
drwxr-xr-x 3 yanglei staff 96B Jul 19 17:34 pkgconfig

执行之后,查看文件已存在,同时执行ffmpeg,发现已经可以正常使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码$ ffmpeg
ffmpeg version 4.1.4 Copyright (c) 2000-2019 the FFmpeg developers
built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.4_1 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags='-I/Library/Java/JavaVirtualMachines/adoptopenjdk-12.0.1.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-12.0.1.jdk/Contents/Home/include/darwin' --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
libavutil 56. 22.100 / 56. 22.100
libavcodec 58. 35.100 / 58. 35.100
libavformat 58. 20.100 / 58. 20.100
libavdevice 58. 5.100 / 58. 5.100
libavfilter 7. 40.101 / 7. 40.101
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 3.100 / 5. 3.100
libswresample 3. 3.100 / 3. 3.100
libpostproc 55. 3.100 / 55. 3.100
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...

Use -h to get full help or, even bet

来个视频试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码$ ffmpeg -i bb.mp4 
ffmpeg version 4.1.4 Copyright (c) 2000-2019 the FFmpeg developers
built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.4_1 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags='-I/Library/Java/JavaVirtualMachines/adoptopenjdk-12.0.1.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-12.0.1.jdk/Contents/Home/include/darwin' --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
libavutil 56. 22.100 / 56. 22.100
libavcodec 58. 35.100 / 58. 35.100
libavformat 58. 20.100 / 58. 20.100
libavdevice 58. 5.100 / 58. 5.100
libavfilter 7. 40.101 / 7. 40.101
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 3.100 / 5. 3.100
libswresample 3. 3.100 / 3. 3.100
libpostproc 55. 3.100 / 55. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'bb.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf56.25.101
Duration: 00:00:11.24, start: 0.000000, bitrate: 1182 kb/s
Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 1180 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)
Metadata:
handler_name : VideoHandler

目前ffmpeg使用一切顺利,然鹅其他软件又出现了php。。。欲解决问题,请看我的下篇文章【php执行失败动态库libicui18n报错】

本文转载自: 掘金

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

推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化

发表于 2019-07-18

一. JVM内存区域的划分

1.1 java虚拟机运行时数据区

java虚拟机运行时数据区分布图:

  • JVM栈(Java Virtual Machine Stacks): Java中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈,因此栈存储的信息都是跟当前线程(或程序)相关信息的,包括局部变量、程序运行状态、方法返回值、方法出口等等。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 堆(Heap): 堆是所有线程共享的,主要是存放对象实例和数组。处于物理上不连续的内存空间,只要逻辑连续即可
  • 方法区(Method Area): 属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 常量池(Runtime Constant Pool): 它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
  • 本地方法栈(Native Method Stacks):## 一. JVM内存区域的划分

1.1 java虚拟机运行时数据区

java虚拟机运行时数据区分布图:

  • JVM栈(Java Virtual Machine Stacks): Java中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈,因此栈存储的信息都是跟当前线程(或程序)相关信息的,包括局部变量、程序运行状态、方法返回值、方法出口等等。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 堆(Heap): 堆是所有线程共享的,主要是存放对象实例和数组。处于物理上不连续的内存空间,只要逻辑连续即可
  • 方法区(Method Area): 属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 常量池(Runtime Constant Pool): 它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
  • 本地方法栈(Native Method Stacks):

其中,堆(Heap)和JVM栈是程序运行的关键,因为:

  1. 栈是运行时的单位(解决程序的运行问题,即程序如何执行,或者说如何处理数据),而堆是存储的单位(解决的是数据存储的问题,即数据怎么放、放在哪儿)。
  2. 堆存储的是对象。栈存储的是基本数据类型和堆中对象的引用;(参数传递的值传递和引用传递)

那为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

  1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据,分工明确,处理逻辑更为清晰体现了“分而治之”以及“隔离”的思想。
  2. 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这样共享的方式有很多收益:提供了一种有效的数据交互方式(如:共享内存);堆中的共享常量和缓存可以被所有栈访问,节省了空间。
  3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
  4. 堆和栈的结合完美体现了面向对象的设计。当我们将对象拆开,你会发现,对象的属性即是数据,存放在堆中;而对象的行为(方法)即是运行逻辑,放在栈中。因此编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。

1.2 堆(Heap)和JVM栈:

1.2.1 堆(Heap)

  Java堆是java虚拟机所管理内存中最大的一块内存空间,处于物理上不连续的内存空间,只要逻辑连续即可,主要用于存放各种类的实例对象。该区域被所有线程共享,在虚拟机启动时创建,用来存放对象的实例,几乎所有的对象以及数组都在这里分配内存(栈上分配、标量替换优化技术的例外)。

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor(S0)、To Survivor(S1)。如图所示:

堆的内存布局:

  这样划分的目的是为了使jvm能够更好的管理内存中的对象,包括内存的分配以及回收。 而新生代按eden和两个survivor的分法,是为了

  • 有效空间增大,eden+1个survivor;
  • 有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其挪到老年代中,即只需扫描其中一个survivor。如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放。
  • 两个Survivor区可解决内存碎片化

1.2.2 堆栈相关的参数

参数 描述
-Xms 堆内存初始大小,单位m、g
-Xmx 堆内存最大允许大小,一般不要大于物理内存的80%
-Xmn 年轻代内存初始大小
-Xss 每个线程的堆栈大小,即JVM栈的大小
-XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值
-XX:NewSzie(-Xns) 年轻代内存初始大小,可以缩写-Xns
-XX:MaxNewSize(-Xmx) 年轻代内存最大允许大小,可以缩写-Xmx
-XX:SurvivorRatio 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1
-XX:MinHeapFreeRatio GC后,如果发现空闲堆内存占到整个预估堆内存的40%,则放大堆内存的预估最大值,但不超过固定最大值。
-XX:MaxHeapFreeRatio 预估堆内存是堆大小动态调控的重要选项之一。堆内存预估最大值一定小于或等于固定最大值(-Xmx指定的数值)。前者会根据使用情况动态调大或缩小,以提高GC回收的效率,默认70%
-XX:MaxTenuringThreshold 垃圾最大年龄,设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
-XX:InitialTenuringThreshold 可以设定老年代阀值的初始值
-XX:+PrintTenuringDistribution 查看每次minor GC后新的存活周期的阈值

Note: 每次GC 后会调整堆的大小,为了防止动态调整带来的性能损耗,一般设置-Xms、-Xmx 相等。

新生代的三个设置参数:-Xmn,-XX:NewSize,-XX:NewRatio的优先级:

(1).最高优先级: -XX:NewSize=1024m和-XX:MaxNewSize=1024m

(2).次高优先级: -Xmn1024m (默认等效效果是:-XX:NewSize==-XX:MaxNewSize==1024m)

(3).最低优先级:-XX:NewRatio=2

推荐使用的是-Xmn参数,原因是这个参数很简洁,相当于一次性设定NewSize和MaxNewSIze,而且两者相等。

1.3 jvm对象

1.3.1 创建对象的方式


各个方式的实质操作如下:

方式 实质
使用new关键 调用无参或有参构造器函数创建
使用Class的newInstance方法 调用无参或有参构造器函数创建,且需要是publi的构造函数
使用Constructor类的newInstance方法 调用有参和私有private构造器函数创建,实用性更广
使用Clone方法 不调用任何参构造器函数,且对象需要实现Cloneable接口并实现其定义的clone方法,且默认为浅复制
第三方库Objenesis 利用了asm字节码技术,动态生成Constructor对象

1.3.2 jvm对象分配

在虚拟机层面上创建对象的步骤:

步骤 解析
1、判断对象对应的类是否加载、链接、初始化 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么必须先执行类的加载、解释、初始化(类的clinit方法)。
2、为对象分配内存 类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从Java堆中划分出一块确定大小的内存而已。
3、处理并发安全问题 另外一个问题及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。 虚拟机采用了两种方式解决并发问题:(1)CAS配上失败重试的方式保证指针更新操作的原子性;(2)TLAB 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer)虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
4、初始化分配到的空间 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值
5、设置对象的对象头 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC分代年龄等数据存储在对象的对象头中
6、执行init方法进行初始化 在Java程序的视角看来,初始化才正式开始,开始调用方法完成初始赋值和构造函数,所有的字段都为零值。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执 行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

1.3.3 对象分配内存方式

分配对象内存,有两种分配方式,指针碰撞和空闲列表:

(1)如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。
如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。
一般使用带有compact(整理)过程的收集器时,使用指针碰撞。

(2)如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List)”。

Note: 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

1.3.4 那什么样的对象能够进入老年代(Old)

那什么样的对象能够进入老年代(Old)?

1.4 内存分配与回收策略

情况 解析
1.对象优先在Eden分配 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC;虚拟机提供了-XX:PrintGCDetails参数,发生垃圾回收时打印内存回收日志,并且在进程退出时输出当前内存各区域的分配情况。
2.大对象直接进入老年代 所谓的大对象就是指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串及数组。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值得对象直接在老年代中分配(这样做的目的是避免在Eden区及两个Survivor之间发生大量的内存拷贝)
3.长期存活的对象将直接进入老年代 对象年龄计数器。-XX:MaxTenuringThreshold
4、动态对象年龄判定 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
5、空间分配担保 在发生Minor GC时(前),虚拟机会检测之前每次晋升到老年代的平均大小(因为当次会有多少对象会存活是无法确定的,所以取之前的平均值/经验值)是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。取平均值进行比较其实仍然是一种动态概率手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure),这样会触发Full GC。

二 垃圾回收算法分类

2.1 引用

2.2 GC Root的对象

2.3 标记-清除(Mark—Sweep)

被誉为现代垃圾回收算法的思想基础。


  标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

2.4 复制算法(Copying)

  该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。建立在存活对象少,垃圾对象多的前提下。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去后还能进行相应的内存整理,不会出现碎片问题。但缺点也是很明显,就是需要两倍内存空间。

  它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

2.5 标记-整理(或标记-压缩算法,Mark-Compact,又或者叫标记清除压缩MarkSweepCompact)

  此算法是结合了“标记-清除”和“复制算法”两个算法的优点。避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。


标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

2.6 分代回收策略(Generational Collecting)

  基于这样的事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

  新生代由于其对象存活时间短,且需要经常gc,因此采用效率较高的复制算法,其将内存区分为一个eden区和两个suvivor区,默认eden区和survivor区的比例是8:1,分配内存时先分配eden区,当eden区满时,使用复制算法进行gc,将存活对象复制到一个survivor区,当一个survivor区满时,将其存活对象复制到另一个区中,当对象存活时间大于某一阈值时,将其放入老年代。老年代和永久代因为其存活对象时间长,因此使用标记清除或标记整理算法

总结:

  • 新生代:复制算法(新生代回收的频率很高,每次回收的耗时很短,为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表(Card Table)的数据结构,卡表为一个比特位集合,每个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对,

2.7 垃圾回收器


垃圾回收器的任务是识别和回收垃圾对象进行内存清理,不同代可使用不同的收集器:

  • 新生代收集器使用的收集器:Serial、ParNew、Parallel Scavenge;
  • 老年代收集器使用的收集器:Serial Old(MSC)、Parallel Old、CMS。

总结:

  1. Serial old和新生代的所有回收器都能搭配;也可以作为CMS回收器的备用回收器;
  2. CMS只能和新生代的Serial和ParNew搭配,而且ParNew是CMS默认的新生代回收器;
  3. 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
  4. 并发(Concurrent):指用户线程和垃圾收集线程同时执行(但不一定是并行的,可能是交替执行),用户程序继续运行,而垃圾收集程序运行在另外的CPU上。

三. GC的执行机制

  Java 中的堆(deap) 也是 GC 收集垃圾的主要区域。
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC(Minor GC)和Full GC(Major GC)。

  • Scavenge GC(Minor GC): 一般情况下,当新对象生成(age=0),并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区(age+1)。然后整理(其实是复制过去就顺便整理了)Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法(即复制-清理算法),使Eden去能尽快空闲出来。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。
  • Full GC:
    对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。

3.1 触发Full GC执行的场景

3.2 Young GC触发条件

3.3 新生对象GC收回流程

  基于大多数新生对象都会在GC中被收回的假设。新生代的GC 使用复制算法,(将年轻代分为3部分,主要是为了生命周期短的对象尽量留在年轻代。老年代主要存放生命周期比较长的对象,比如缓存)。可能经历过程:

  1. 对象创建时,一般在Eden区完成内存分配(有特殊);
  2. 当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收;
  3. minorGC时,Eden和survivor A不能被GC回收且年龄没有达到阈值(tenuring threshold)的对象,会被放入survivor B,始终保证一个survivor是空的;
  4. 当做第3步的时候,如果发现survivor满了,将这些对象copy到old区(分配担保机制);或者survivor并没有满,但是有些对象已经足够Old,也被放入Old区 XX:MaxTenuringThreshold;(回顾下对象进入老年代的情况)
  5. 直接清空eden和survivor A;
  6. 当Old区被放满的之后,进行fullGC。

3.4 GC日志

GC日志相关参数:

  • -XX:+PrintGC:输出GC日志
  • -XX:+PrintGCDetails:输出GC的详细日志
  • -XX:+PrintGCTimeStamps:输出GC的时间戳(以基准时间的形式)
  • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间
  • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间
  • -XX:+PrintHeapAtGC:在进行GC的前后打印出堆的信息
  • -XX:+PrintTLAB:查看TLAB空间的使用情况
  • -XX:PrintTenuingDistribution:查看每次minor GC后新的存活周期的阈值
  • -XX:PrintReferenceFC:用来跟踪系统内的(softReference)软引用,(weadReference)弱引用,(phantomReference)虚引用,显示引用过程

案例分析:-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime一起使用

1
2
3
4
less复制代码    Application time: 0.3440086 seconds
Total time for which application threads were stopped: 0.0620105 seconds
Application time: 0.2100691 seconds
Total time for which application threads were stopped: 0.0890223 seconds

得知应用程序在前344毫秒中是在处理实际工作的,然后将所有线程暂停了62毫秒,紧接着又工作了210ms,然后又暂停了89ms。

1
2
scss复制代码2796146K->2049K(1784832K)] 4171400K->2049K(3171840K), [Metaspace: 3134K->3134K(1056768K)], 0.0571841 secs] [Times: user=0.02 sys=0.04, real=0.06 secs]
Total time for which application threads were stopped: 0.0572646 seconds, Stopping threads took: 0.0000088 seconds

应用线程被强制暂停了57ms来进行垃圾回收。其中又有8ms是用来等待所有的应用线程都到达安全点。

只要设置-XX:+PrintGCDetails 就会自动带上-verbose:gc和-XX:+PrintGC

1
2
yaml复制代码33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]    
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
  1. 最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
  2. GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的。
  3. 接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。
  4. 后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”。而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。
  5. 再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据
  6. [Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs]
    新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC (System)”。

3.5 减少GC开销的措施

从代码上:

从JVM参数上调优上:

3.6 内存溢出分类

四. 总结-JVM调优相关

4.1 调优目的

,

4.2 JVM性能调优所处的层次

4.3 JVM调优流程

4.4 性能监控工具


  调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm的调优也不例外,jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。

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

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

本文转载自: 掘金

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

「Java8系列」神奇的函数式接口

发表于 2019-07-18

前言

在上一篇Lambda的讲解中我们就提到过函数式接口,比如:Consumer<String> consumer = (s) -> System.out.println(s);其中Consumer就是一个函数式接口。这里是通过Lambda表达式创建了一个函数式接口的对象。如果不知道什么是Lambda,请看《神秘的Lambda》。

函数式接口是什么?

有且只有一个抽象方法的接口被称为函数式接口,函数式接口适用于函数式编程的场景,Lambda就是Java中函数式编程的体现,可以使用Lambda表达式创建一个函数式接口的对象,一定要确保接口中有且只有一个抽象方法,这样Lambda才能顺利的进行推导。

@FunctionalInterface注解

与@Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。但是这个注解不是必须的,只要符合函数式接口的定义,那么这个接口就是函数式接口。

static方法和default方法

实在不知道该在哪介绍这两个方法了,所以就穿插在这里了。

static方法:

java8中为接口新增了一项功能,定义一个或者多个静态方法。用法和普通的static方法一样,例如:

1
2
3
4
5
6
7
8
csharp复制代码public interface Interface {
/**
* 静态方法
*/
static void staticMethod() {
System.out.println("static method");
}
}

注意:实现接口的类或者子接口不会继承接口中的静态方法。

default方法:

java8在接口中新增default方法,是为了在现有的类库中中新增功能而不影响他们的实现类,试想一下,如果不增加默认实现的话,接口的所有实现类都要实现一遍这个方法,这会出现兼容性问题,如果定义了默认实现的话,那么实现类直接调用就可以了,并不需要实现这个方法。default方法怎么定义?

1
2
3
4
5
6
7
8
csharp复制代码public interface Interface {
/**
* default方法
*/
default void print() {
System.out.println("hello default");
}
}

注意:如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。不用加default关键字,例如:

1
2
3
4
5
6
typescript复制代码public class InterfaceImpl implements Interface {
@Override
public void print() {
System.out.println("hello default 2");
}
}

在函数式接口的定义中是只允许有一个抽象方法,但是可以有多个static方法和default方法。

自定义函数式接口

按照下面的格式定义,你也能写出函数式接口:

1
2
3
4
5
scss复制代码 @FunctionalInterface
修饰符 interface 接口名称 {
返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}

虽然@FunctionalInterface注解不是必须的,但是自定义函数式接口最好还是都加上,一是养成良好的编程习惯,二是防止他人修改,一看到这个注解就知道是函数式接口,避免他人往接口内添加抽象方法造成不必要的麻烦。

1
2
3
4
java复制代码@FunctionalInterface
public interface MyFunction {
void print(String s);
}

看上图是我自定义的一个函数式接口,那么这个接口的作用是什么呢?就是输出一串字符串,属于消费型接口,是模仿Consumer接口写的,只不过这个没有使用泛型,而是将参数具体类型化了,不知道Consumer没关系,下面会介绍到,其实java8中提供了很多常用的函数式接口,Consumer就是其中之一,一般情况下都不需要自己定义,直接使用就好了。那么怎么使用这个自定义的函数式接口呢?我们可以用函数式接口作为参数,调用时传递Lambda表达式。如果一个方法的参数是Lambda,那么这个参数的类型一定是函数式接口。例如:

1
2
3
4
5
6
7
8
9
10
arduino复制代码public class MyFunctionTest {
public static void main(String[] args) {
String text = "试试自定义函数好使不";
printString(text, System.out::print);
}

private static void printString(String text, MyFunction myFunction) {
myFunction.print(text);
}
}

执行以后就会输出“试试自定义函数好使不”这句话,如果某天需求变了,我不想输出这句话了,想输出别的,那么直接替换text就好了。函数式编程是没有副作用的,最大的好处就是函数的内部是无状态的,既输入确定输出就确定。函数式编程还有更多好玩的套路,这就需要靠大家自己探索了。😝

常用函数式接口

Consumer<T>:消费型接口

抽象方法: void accept(T t),接收一个参数进行消费,但无需返回结果。

使用方式:

1
2
ini复制代码  Consumer consumer = System.out::println;
consumer.accept("hello function");

默认方法: andThen(Consumer<? super T> after),先消费然后在消费,先执行调用andThen接口的accept方法,然后在执行andThen方法参数after中的accept方法。

使用方式:

1
2
3
4
5
6
7
ini复制代码  Consumer<String> consumer1 = s -> System.out.print("车名:"+s.split(",")[0]);
Consumer<String> consumer2 = s -> System.out.println("-->颜色:"+s.split(",")[1]);

String[] strings = {"保时捷,白色", "法拉利,红色"};
for (String string : strings) {
consumer1.andThen(consumer2).accept(string);
}

输出:
车名:保时捷–>颜色:白色
车名:法拉利–>颜色:红色

Supplier<T>: 供给型接口

抽象方法:T get(),无参数,有返回值。

使用方式:

1
2
ini复制代码 Supplier<String> supplier = () -> "我要变的很有钱";
System.out.println(supplier.get());

最后输出就是“我要变得很有钱”,这类接口适合提供数据的场景。

Function<T,R>: 函数型接口

抽象方法: R apply(T t),传入一个参数,返回想要的结果。

使用方式:

1
2
ini复制代码 Function<Integer, Integer> function1 = e -> e * 6;
System.out.println(function1.apply(2));

很简单的一个乘法例子,显然最后输出是12。

默认方法:

  • compose(Function<? super V, ? extends T> before),先执行compose方法参数before中的apply方法,然后将执行结果传递给调用compose函数中的apply方法在执行。

使用方式:

1
2
3
4
5
ini复制代码 Function<Integer, Integer> function1 = e -> e * 2;
Function<Integer, Integer> function2 = e -> e * e;

Integer apply2 = function1.compose(function2).apply(3);
System.out.println(apply2);

还是举一个乘法的例子,compose方法执行流程是先执行function2的表达式也就是33=9,然后在将执行结果传给function1的表达式也就是92=18,所以最终的结果是18。

  • andThen(Function<? super R, ? extends V> after),先执行调用andThen函数的apply方法,然后在将执行结果传递给andThen方法after参数中的apply方法在执行。它和compose方法整好是相反的执行顺序。

使用方式:

1
2
3
4
5
ini复制代码 Function<Integer, Integer> function1 = e -> e * 2;
Function<Integer, Integer> function2 = e -> e * e;

Integer apply3 = function1.andThen(function2).apply(3);
System.out.println(apply3);

这里我们和compose方法使用一个例子,所以是一模一样的例子,由于方法的不同,执行顺序也就不相同,那么结果是大大不同的。andThen方法是先执行function1表达式,也就是32=6,然后在执行function2表达式也就是66=36。结果就是36。
**静态方法:**identity(),获取一个输入参数和返回结果相同的Function实例。

使用方式:

1
2
3
ini复制代码 Function<Integer, Integer> identity = Function.identity();
Integer apply = identity.apply(3);
System.out.println(apply);

平常没有遇到过使用这个方法的场景,总之这个方法的作用就是输入什么返回结果就是什么。

Predicate<T> : 断言型接口

抽象方法: boolean test(T t),传入一个参数,返回一个布尔值。

使用方式:

1
2
3
ini复制代码 Predicate<Integer> predicate = t -> t > 0;
boolean test = predicate.test(1);
System.out.println(test);

当predicate函数调用test方法的时候,就会执行拿test方法的参数进行t -> t > 0的条件判断,1肯定是大于0的,最终结果为true。

默认方法:

  • and(Predicate<? super T> other),相当于逻辑运算符中的&&,当两个Predicate函数的返回结果都为true时才返回true。

使用方式:

1
2
3
4
ini复制代码 Predicate<String> predicate1 = s -> s.length() > 0;
Predicate<String> predicate2 = Objects::nonNull;
boolean test = predicate1.and(predicate2).test("&&测试");
System.out.println(test);
  • or(Predicate<? super T> other) ,相当于逻辑运算符中的||,当两个Predicate函数的返回结果有一个为true则返回true,否则返回false。

使用方式:

1
2
3
4
ini复制代码 Predicate<String> predicate1 = s -> false;
Predicate<String> predicate2 = Objects::nonNull;
boolean test = predicate1.and(predicate2).test("||测试");
System.out.println(test);
  • negate(),这个方法的意思就是取反。

使用方式:

1
2
3
ini复制代码 Predicate<String> predicate = s -> s.length() > 0;
boolean result = predicate.negate().test("取反");
System.out.println(result);

很明显正常执行test方法的话应该为true,但是调用negate方法后就返回为false了。
**静态方法:**isEqual(Object targetRef),对当前操作进行”=”操作,即取等操作,可以理解为 A == B。

使用方式:

1
2
3
4
ini复制代码 boolean test1 = Predicate.isEqual("test").test("test");
boolean test2 = Predicate.isEqual("test").test("equal");
System.out.println(test1); //true
System.out.println(test2); //false

其他函数式接口

Bi类型接口

BiConsumer、BiFunction、BiPrediate 是 Consumer、Function、Predicate 的扩展,可以传入多个参数,没有 BiSupplier 是因为 Supplier 没有入参。

操作基本数据类型的接口

IntConsumer、IntFunction、IntPredicate、IntSupplier、LongConsumer、LongFunction、LongPredicate、LongSupplier、DoubleConsumer、DoubleFunction、DoublePredicate、DoubleSupplier。
其实常用的函数式接口就那四大接口Consumer、Function、Prediate、Supplier,其他的函数式接口就不一一列举了,有兴趣的可以去java.util.function这个包下详细的看。

大家看后辛苦点个赞点个关注哦!后续还会后更多的博客。如有错误,烦请指正。

本文转载自: 掘金

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

Android不同状态页面管理优化

发表于 2019-07-17

目录介绍

  • 01.界面状态有哪些
  • 02.采用include方式管理
  • 03.在Base类中处理逻辑
  • 04.如何降低偶性和入侵性
  • 05.封装低入侵性状态库
    • 5.1 自定义帧布局
    • 5.2 自定义状态管理器
    • 5.3 如何管理多种状态
  • 06.封装库极致优化点说明
    • 6.1 用ViewStub显示布局
    • 6.2 处理重新加载逻辑
  • 07.如何使用该封装库

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01.界面状态有哪些

  • 在Android中,不管是activity或者fragment,在加载视图的时候都有可能会出现多种不同的状态页面View。比如常见的就有这些:
    • 内容界面,也就是正常有数据页面
    • 加载数据中,加载loading
    • 加载数据错误,请求数据异常
    • 加载后没有数据,请求数据为空
    • 没有网络,网络异常
  • 同时,思考一下几个问题。
    • 怎样切换界面状态?有些界面想定制自定义状态?状态如何添加点击事件?下面就为解决这些问题!
  • 为何要这样?
    • 一般在加载网络数据时,需要用户等待的场景,显示一个加载的Loading动画可以让用户知道App正在加载数据,而不是程序卡死,从而给用户较好的使用体验。
    • 当加载的数据为空时显示一个数据为空的视图、在数据加载失败时显示加载失败对应的UI并支持点击重试会比白屏的用户体验更好一些。
    • 加载中、加载失败、空数据等不同状态页面风格,一般来说在App内的所有页面中需要保持一致,也就是需要做到全局统一。

02.采用include方式管理

  • 直接把这些界面include到main界面中,然后动态去切换界面,具体一点的做法如下所示。

    • 在布局中,会存放多个状态的布局。然后在页面中根据逻辑将对应的布局给显示或者隐藏,但存在诸多问题。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      复制代码<?xml version="1.0" encoding="utf-8"?>
      <LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/activity_main"
      android:orientation="vertical"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

      <!--正常时布局-->
      <include layout="@layout/activity_content"/>
      <!--加载loading布局-->
      <include layout="@layout/activity_loading"/>
      <!--异常时布局-->
      <include layout="@layout/activity_error"/>
      <!--空数据时布局-->
      <include layout="@layout/activity_emptydata"/>

      </LinearLayout>
  • 存在的问题分析

    • 后来发现这样处理不容易复用到其他项目中,代码复用性很低
    • 在activity中处理这些状态的显示和隐藏比较乱
    • 调用setContentView方法时,是将所有的布局给加载绘制出来。其实没有必要
    • 如果将逻辑写在BaseActivity中,利用子类继承父类特性,在父类中写切换状态,但有些界面如果没有继承父类,又该如何处理

03.在Base类中处理逻辑

  • 首先是定义一个自定义的控件,比如把它命名成LoadingView,然后在这个里面include一个布局,该布局包含一些不同状态的视图。代码思路如下所示:

    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
    复制代码public class LoadingView extends LinearLayout implements View.OnClickListener {

    public static final int LOADING = 0;
    public static final int STOP_LOADING = 1;
    public static final int NO_DATA = 2;
    public static final int NO_NETWORK = 3;
    public static final int GONE = 4;
    public static final int LOADING_DIALOG = 5;

    private TextView mNoDataTextView;
    private ProgressBar mLoadingProgressBar;
    private RelativeLayout mRlError;
    private LinearLayout mLlLoading;
    private View mView;

    private OnRefreshListener mListener;

    public void setRefrechListener(OnRefreshListener mListener) {
    this.mListener = mListener;
    }

    public interface OnRefreshListener {
    void refresh();
    }

    public LoadingView(Context context) {
    super(context);
    init(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
    }

    private void init(Context context) {
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mView = inflater.inflate(R.layout.common_loading_get, this);
    mLoadingProgressBar = (ProgressBar) mView.findViewById(R.id.mLoadingProgressBar);
    mNoDataTextView = (TextView) mView.findViewById(R.id.mNoDataTextView);
    mLlLoading = (LinearLayout) mView.findViewById(R.id.ll_loading);
    mRlError = (RelativeLayout) mView.findViewById(R.id.rl_error);
    mRlError.setOnClickListener(this);
    setStatue(GONE);
    }

    public void setStatue(int status) {
    setVisibility(View.VISIBLE);
    try {
    if (status == LOADING) {//更新
    mRlError.setVisibility(View.GONE);
    mLlLoading.setVisibility(View.VISIBLE);
    } else if (status == STOP_LOADING) {
    setVisibility(View.GONE);
    } else if (status == NO_DATA) {//无数据情况
    mRlError.setVisibility(View.VISIBLE);
    mLlLoading.setVisibility(View.GONE);
    mNoDataTextView.setText("暂无数据");
    } else if (status == NO_NETWORK) {//无网络情况
    mRlError.setVisibility(View.VISIBLE);
    mLlLoading.setVisibility(View.GONE);
    mNoDataTextView.setText("网络加载失败,点击重新加载");
    } else {
    setVisibility(View.GONE);
    }
    } catch (OutOfMemoryError e) {
    }
    }

    @Override
    public void onClick(View v) {
    mListener.refresh();
    setStatue(LOADING);
    }
    }
  • 然后在BaseActivity/BaseFragment中封装LoadingView的初始化逻辑,并封装加载状态切换时的UI显示逻辑,暴露给子类以下方法:

    1
    2
    3
    4
    复制代码void showLoading(); //调用此方法显示加载中的动画
    void showLoadFailed(); //调用此方法显示加载失败界面
    void showEmpty(); //调用此方法显示空页面
    void onClickRetry(); //子类中实现,点击重试的回调方法
    • 在BaseActivity/BaseFragment的子类中可通过上一步的封装比较方便地使用加载状态显示功能。这种使用方式耦合度太高,每个页面的布局文件中都需要添加LoadingView,使用起来不方便而且维护成本较高,比如说有时候异常状态的布局各个页面不同,那么难以自定义处理,修改起来成本较高。
    • 同时如果是要用这种状态管理工具,则需要在需要的页面布局中添加该LoadingView视图。这样也能够完成需求,但是感觉有点麻烦。
  • 具体如何使用它进行状态管理呢?可以看到在对应的布局中需要写上LoadingView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    复制代码<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.cheoo.app.view.recyclerview.TypeRecyclerView
    android:id="@+id/mRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:overScrollMode="never"
    android:scrollbars="none">

    </com.cheoo.app.view.recyclerview.TypeRecyclerView>

    <com.cheoo.app.view.LoadingView
    android:id="@+id/mLoadingView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    </RelativeLayout>
  • 那么,如果某个子类不想继承BaseActivity类,如何使用该状态管理器呢?代码中可以这种使用。

    1
    2
    3
    4
    5
    复制代码mLoadingView  = (LoadingView)findViewById(R.id.mLoadingView);
    mLoadingView.setStatue(LoadingView.LOADING);
    mLoadingView.setStatue(LoadingView.STOP_LOADING);
    mLoadingView.setStatue(LoadingView.NO_NETWORK);
    mLoadingView.setStatue(LoadingView.NO_DATA);

04.如何降低偶性和入侵性

  • 让View状态的切换和Activity彻底分离开,必须把这些状态View都封装到一个管理类中,然后暴露出几个方法来实现View之间的切换。
    在不同的项目中可以需要的View也不一样,所以考虑把管理类设计成builder模式来自由的添加需要的状态View。
  • 那么如何降低耦合性,让代码入侵性低。方便维护和修改,且移植性强呢?大概具备这样的条件……
    • 可以运用在activity或者fragment中
    • 不需要在布局中添加LoadingView,而是统一管理不同状态视图,同时暴露对外设置自定义状态视图方法,方便UI特定页面定制
    • 支持设置自定义不同状态视图,即使在BaseActivity统一处理状态视图管理,也支持单个页面定制
    • 在加载视图的时候像异常和空页面能否用ViewStub代替,这样减少绘制,只有等到出现异常和空页面时,才将视图给inflate出来
    • 当页面出现网络异常页面,空页面等,页面会有交互事件,这时候可以设置点击设置网络或者点击重新加载等等

05.封装低入侵性状态库

5.1 自定义帧布局

  • 首先需要自定义一个状态StateFrameLayout布局,它是继承FrameLayout。在这个类中,目前是设置五种不同状态的视图布局,主要的功能操作是显示或者隐藏布局。为了后期代码维护性,根据面向对象的思想,类尽量保证单一职责,所以关于状态切换,以及设置自定义状态布局,把这个功能分离处理,放到一个StateLayoutManager中处理。
    • 看代码可知,这个类的功能非常明确,就是隐藏或者展示视图作用。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      复制代码/**
      * <pre>
      * @author yangchong
      * blog : https://github.com/yangchong211/YCStateLayout
      * time : 2017/7/6
      * desc : 自定义帧布局
      * revise:
      * </pre>
      */
      public class StateFrameLayout extends FrameLayout {

      /**
      * loading 加载id
      */
      public static final int LAYOUT_LOADING_ID = 1;

      /**
      * 内容id
      */
      public static final int LAYOUT_CONTENT_ID = 2;

      /**
      * 异常id
      */
      public static final int LAYOUT_ERROR_ID = 3;

      /**
      * 网络异常id
      */
      public static final int LAYOUT_NETWORK_ERROR_ID = 4;

      /**
      * 空数据id
      */
      public static final int LAYOUT_EMPTY_DATA_ID = 5;

      /**
      * 存放布局集合
      */
      private SparseArray<View> layoutSparseArray = new SparseArray<>();

      //private HashMap<Integer,View> map = new HashMap<>();

      /**
      * 布局管理器
      */
      private StateLayoutManager mStatusLayoutManager;


      public StateFrameLayout(Context context) {
      super(context);
      }

      public StateFrameLayout(Context context, AttributeSet attrs) {
      super(context, attrs);
      }

      public StateFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
      super(context, attrs, defStyleAttr);
      }


      public void setStatusLayoutManager(StateLayoutManager statusLayoutManager) {
      mStatusLayoutManager = statusLayoutManager;
      //添加所有的布局到帧布局
      addAllLayoutToRootLayout();
      }

      private void addAllLayoutToRootLayout() {
      if (mStatusLayoutManager.contentLayoutResId != 0) {
      addLayoutResId(mStatusLayoutManager.contentLayoutResId, StateFrameLayout.LAYOUT_CONTENT_ID);
      }
      if (mStatusLayoutManager.loadingLayoutResId != 0) {
      addLayoutResId(mStatusLayoutManager.loadingLayoutResId, StateFrameLayout.LAYOUT_LOADING_ID);
      }

      if (mStatusLayoutManager.emptyDataVs != null) {
      addView(mStatusLayoutManager.emptyDataVs);
      }
      if (mStatusLayoutManager.errorVs != null) {
      addView(mStatusLayoutManager.errorVs);
      }
      if (mStatusLayoutManager.netWorkErrorVs != null) {
      addView(mStatusLayoutManager.netWorkErrorVs);
      }
      }

      private void addLayoutResId(@LayoutRes int layoutResId, int id) {
      View resView = LayoutInflater.from(mStatusLayoutManager.context).inflate(layoutResId, null);
      layoutSparseArray.put(id, resView);
      addView(resView);
      }

      /**
      * 显示loading
      */
      public void showLoading() {
      if (layoutSparseArray.get(LAYOUT_LOADING_ID) != null) {
      showHideViewById(LAYOUT_LOADING_ID);
      }
      }

      /**
      * 显示内容
      */
      public void showContent() {
      if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null) {
      showHideViewById(LAYOUT_CONTENT_ID);
      }
      }

      /**
      * 显示空数据
      */
      public void showEmptyData(int iconImage, String textTip) {
      if (inflateLayout(LAYOUT_EMPTY_DATA_ID)) {
      showHideViewById(LAYOUT_EMPTY_DATA_ID);
      emptyDataViewAddData(iconImage, textTip);
      }
      }


      /**
      * 根据ID显示隐藏布局
      * @param id id值
      */
      private void showHideViewById(int id) {
      for (int i = 0; i < layoutSparseArray.size(); i++) {
      int key = layoutSparseArray.keyAt(i);
      View valueView = layoutSparseArray.valueAt(i);
      //显示该view
      if(key == id) {
      valueView.setVisibility(View.VISIBLE);
      if(mStatusLayoutManager.onShowHideViewListener != null) {
      mStatusLayoutManager.onShowHideViewListener.onShowView(valueView, key);
      }
      } else {
      if(valueView.getVisibility() != View.GONE) {
      valueView.setVisibility(View.GONE);
      if(mStatusLayoutManager.onShowHideViewListener != null) {
      mStatusLayoutManager.onShowHideViewListener.onHideView(valueView, key);
      }
      }
      }
      }
      }

      /**
      * 这个是处理ViewStub的逻辑,主要有网络异常布局,加载异常布局,空数据布局
      * @param id 布局id
      * @return 布尔值
      */
      private boolean inflateLayout(int id) {
      boolean isShow = true;
      //如果为null,则直接返回false
      if (layoutSparseArray.get(id) == null) {
      return false;
      }
      switch (id) {
      case LAYOUT_NETWORK_ERROR_ID:
      if (mStatusLayoutManager.netWorkErrorVs != null) {
      View view = mStatusLayoutManager.netWorkErrorVs.inflate();
      retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
      layoutSparseArray.put(id, view);
      isShow = true;
      } else {
      isShow = false;
      }
      break;
      case LAYOUT_ERROR_ID:
      if (mStatusLayoutManager.errorVs != null) {
      View view = mStatusLayoutManager.errorVs.inflate();
      if (mStatusLayoutManager.errorLayout != null) {
      mStatusLayoutManager.errorLayout.setView(view);
      }
      retryLoad(view, mStatusLayoutManager.errorRetryViewId);
      layoutSparseArray.put(id, view);
      isShow = true;
      } else {
      isShow = false;
      }
      break;
      case LAYOUT_EMPTY_DATA_ID:
      if (mStatusLayoutManager.emptyDataVs != null) {
      View view = mStatusLayoutManager.emptyDataVs.inflate();
      if (mStatusLayoutManager.emptyDataLayout != null) {
      mStatusLayoutManager.emptyDataLayout.setView(view);
      }
      retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);
      layoutSparseArray.put(id, view);
      isShow = true;
      } else {
      isShow = false;
      }
      break;
      default:
      break;
      }
      return isShow;
      }
      }

5.2 自定义状态管理器

  • 上面状态的自定义布局创建出来了,而且隐藏和展示都做了。那么如何控制设置自定义视图布局,还有如何控制不同布局之间切换,那么就需要用到这个类呢!github.com/yangchong21…
    • loadingLayoutResId和contentLayoutResId代表等待加载和显示内容的xml文件
    • 几种异常状态要用ViewStub,因为在界面状态切换中loading和内容View都是一直需要加载显示的,但是其他的3个只有在没数据或者网络异常的情况下才会加载显示,所以用ViewStub来加载他们可以提高性能。
    • 采用builder模式,十分简单,代码如下所示。创建StateFrameLayout对象,然后再设置setStatusLayoutManager,这一步操作是传递一个Manager对象到StateFrameLayout,建立连接。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      复制代码public final class StateLayoutManager {

      final Context context;

      final int netWorkErrorRetryViewId;
      final int emptyDataRetryViewId;
      final int errorRetryViewId;
      final int loadingLayoutResId;
      final int contentLayoutResId;
      final int retryViewId;
      final int emptyDataIconImageId;
      final int emptyDataTextTipId;
      final int errorIconImageId;
      final int errorTextTipId;

      final ViewStub emptyDataVs;
      final ViewStub netWorkErrorVs;
      final ViewStub errorVs;
      final AbsViewStubLayout errorLayout;
      final AbsViewStubLayout emptyDataLayout;

      private final StateFrameLayout rootFrameLayout;
      final OnShowHideViewListener onShowHideViewListener;
      final OnRetryListener onRetryListener;

      public static Builder newBuilder(Context context) {
      return new Builder(context);
      }

      private StateLayoutManager(Builder builder) {
      this.context = builder.context;
      this.loadingLayoutResId = builder.loadingLayoutResId;
      this.netWorkErrorVs = builder.netWorkErrorVs;
      this.netWorkErrorRetryViewId = builder.netWorkErrorRetryViewId;
      this.emptyDataVs = builder.emptyDataVs;
      this.emptyDataRetryViewId = builder.emptyDataRetryViewId;
      this.errorVs = builder.errorVs;
      this.errorRetryViewId = builder.errorRetryViewId;
      this.contentLayoutResId = builder.contentLayoutResId;
      this.onShowHideViewListener = builder.onShowHideViewListener;
      this.retryViewId = builder.retryViewId;
      this.onRetryListener = builder.onRetryListener;
      this.emptyDataIconImageId = builder.emptyDataIconImageId;
      this.emptyDataTextTipId = builder.emptyDataTextTipId;
      this.errorIconImageId = builder.errorIconImageId;
      this.errorTextTipId = builder.errorTextTipId;
      this.errorLayout = builder.errorLayout;
      this.emptyDataLayout = builder.emptyDataLayout;

      //创建帧布局
      rootFrameLayout = new StateFrameLayout(this.context);
      ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
      ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
      rootFrameLayout.setLayoutParams(layoutParams);

      //设置状态管理器
      rootFrameLayout.setStatusLayoutManager(this);
      }


      /**
      * 显示loading
      */
      public void showLoading() {
      rootFrameLayout.showLoading();
      }

      /**
      * 显示内容
      */
      public void showContent() {
      rootFrameLayout.showContent();
      }

      /**
      * 显示空数据
      */
      public void showEmptyData(int iconImage, String textTip) {
      rootFrameLayout.showEmptyData(iconImage, textTip);
      }

      /**
      * 显示空数据
      */
      public void showEmptyData() {
      showEmptyData(0, "");
      }

      /**
      * 显示空数据
      */
      public void showLayoutEmptyData(Object... objects) {
      rootFrameLayout.showLayoutEmptyData(objects);
      }

      /**
      * 显示网络异常
      */
      public void showNetWorkError() {
      rootFrameLayout.showNetWorkError();
      }

      /**
      * 显示异常
      */
      public void showError(int iconImage, String textTip) {
      rootFrameLayout.showError(iconImage, textTip);
      }

      /**
      * 显示异常
      */
      public void showError() {
      showError(0, "");
      }

      public void showLayoutError(Object... objects) {
      rootFrameLayout.showLayoutError(objects);
      }

      /**
      * 得到root 布局
      */
      public View getRootLayout() {
      return rootFrameLayout;
      }

      public static final class Builder {

      private Context context;
      private int loadingLayoutResId;
      private int contentLayoutResId;
      private ViewStub netWorkErrorVs;
      private int netWorkErrorRetryViewId;
      private ViewStub emptyDataVs;
      private int emptyDataRetryViewId;
      private ViewStub errorVs;
      private int errorRetryViewId;
      private int retryViewId;
      private int emptyDataIconImageId;
      private int emptyDataTextTipId;
      private int errorIconImageId;
      private int errorTextTipId;
      private AbsViewStubLayout errorLayout;
      private AbsViewStubLayout emptyDataLayout;
      private OnShowHideViewListener onShowHideViewListener;
      private OnRetryListener onRetryListener;

      Builder(Context context) {
      this.context = context;
      }

      /**
      * 自定义加载布局
      */
      public Builder loadingView(@LayoutRes int loadingLayoutResId) {
      this.loadingLayoutResId = loadingLayoutResId;
      return this;
      }

      /**
      * 自定义网络错误布局
      */
      public Builder netWorkErrorView(@LayoutRes int newWorkErrorId) {
      netWorkErrorVs = new ViewStub(context);
      netWorkErrorVs.setLayoutResource(newWorkErrorId);
      return this;
      }

      /**
      * 自定义加载空数据布局
      */
      public Builder emptyDataView(@LayoutRes int noDataViewId) {
      emptyDataVs = new ViewStub(context);
      emptyDataVs.setLayoutResource(noDataViewId);
      return this;
      }

      /**
      * 自定义加载错误布局
      */
      public Builder errorView(@LayoutRes int errorViewId) {
      errorVs = new ViewStub(context);
      errorVs.setLayoutResource(errorViewId);
      return this;
      }

      /**
      * 自定义加载内容正常布局
      */
      public Builder contentView(@LayoutRes int contentLayoutResId) {
      this.contentLayoutResId = contentLayoutResId;
      return this;
      }

      public Builder errorLayout(AbsViewStubLayout errorLayout) {
      this.errorLayout = errorLayout;
      this.errorVs = errorLayout.getLayoutVs();
      return this;
      }

      public Builder emptyDataLayout(AbsViewStubLayout emptyDataLayout) {
      this.emptyDataLayout = emptyDataLayout;
      this.emptyDataVs = emptyDataLayout.getLayoutVs();
      return this;
      }

      public Builder netWorkErrorRetryViewId(@LayoutRes int netWorkErrorRetryViewId) {
      this.netWorkErrorRetryViewId = netWorkErrorRetryViewId;
      return this;
      }

      public Builder emptyDataRetryViewId(@LayoutRes int emptyDataRetryViewId) {
      this.emptyDataRetryViewId = emptyDataRetryViewId;
      return this;
      }

      public Builder errorRetryViewId(@LayoutRes int errorRetryViewId) {
      this.errorRetryViewId = errorRetryViewId;
      return this;
      }

      public Builder retryViewId(@LayoutRes int retryViewId) {
      this.retryViewId = retryViewId;
      return this;
      }

      public Builder emptyDataIconImageId(@LayoutRes int emptyDataIconImageId) {
      this.emptyDataIconImageId = emptyDataIconImageId;
      return this;
      }

      public Builder emptyDataTextTipId(@LayoutRes int emptyDataTextTipId) {
      this.emptyDataTextTipId = emptyDataTextTipId;
      return this;
      }

      public Builder errorIconImageId(@LayoutRes int errorIconImageId) {
      this.errorIconImageId = errorIconImageId;
      return this;
      }

      public Builder errorTextTipId(@LayoutRes int errorTextTipId) {
      this.errorTextTipId = errorTextTipId;
      return this;
      }

      /**
      * 为状态View显示隐藏监听事件
      * @param listener listener
      * @return
      */
      public Builder onShowHideViewListener(OnShowHideViewListener listener) {
      this.onShowHideViewListener = listener;
      return this;
      }

      /**
      * 为重试加载按钮的监听事件
      * @param onRetryListener listener
      * @return
      */
      public Builder onRetryListener(OnRetryListener onRetryListener) {
      this.onRetryListener = onRetryListener;
      return this;
      }

      /**
      * 创建对象
      * @return
      */
      public StateLayoutManager build() {
      return new StateLayoutManager(this);
      }
      }

      }

5.3 如何管理多种状态

  • 大约5种状态,如何管理这些状态?添加到集合中,Android中选用SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    复制代码/**存放布局集合 */
    private SparseArray<View> layoutSparseArray = new SparseArray();

    /**将布局添加到集合 */
    private void addLayoutResId(@LayoutRes int layoutResId, int id) {
        View resView = LayoutInflater.from(mStatusLayoutManager.context).inflate(layoutResId, null);
        layoutSparseArray.put(id, resView);
        addView(resView);
    }

    //那么哪里从集合中取数据呢
    public void showContent() {
    if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null) {
    showHideViewById(LAYOUT_CONTENT_ID);
    }
    }

06.封装库极致优化点说明

6.1 用ViewStub显示布局

  • 方法里面通过id判断来执行不同的代码,首先判断ViewStub是否为空,如果为空就代表没有添加这个View就返回false,不为空就加载View并且添加到集合当中,然后调用showHideViewById方法显示隐藏View,retryLoad方法是给重试按钮添加事件
    • 注意,即使当你设置了多种不同状态视图,调用setContentView的时候,因为异常页面使用ViewStub,所以在绘制的时候不会影响性能的。
      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
      复制代码/**
      *  显示loading
      */
      public void showLoading() {
          if (layoutSparseArray.get(LAYOUT_LOADING_ID) != null)
              showHideViewById(LAYOUT_LOADING_ID);
      }

      /**
      *  显示内容
      */
      public void showContent() {
          if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null)
              showHideViewById(LAYOUT_CONTENT_ID);
      }

      //调用inflateLayout方法,方法返回true然后调用showHideViewById方法
      private boolean inflateLayout(int id) {
          boolean isShow = true;
          if (layoutSparseArray.get(id) != null) return isShow;
          switch (id) {
              case LAYOUT_NETWORK_ERROR_ID:
                  if (mStatusLayoutManager.netWorkErrorVs != null) {
                      View view = mStatusLayoutManager.netWorkErrorVs.inflate();
                      retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
                      layoutSparseArray.put(id, view);
                      isShow = true;
                  } else {
                      isShow = false;
                  }
                  break;
              case LAYOUT_ERROR_ID:
                  if (mStatusLayoutManager.errorVs != null) {
                      View view = mStatusLayoutManager.errorVs.inflate();
                      if (mStatusLayoutManager.errorLayout != null) mStatusLayoutManager.errorLayout.setView(view);
                      retryLoad(view, mStatusLayoutManager.errorRetryViewId);
                      layoutSparseArray.put(id, view);
                      isShow = true;
                  } else {
                      isShow = false;
                  }
                  break;
              case LAYOUT_EMPTYDATA_ID:
                  if (mStatusLayoutManager.emptyDataVs != null) {
                      View view = mStatusLayoutManager.emptyDataVs.inflate();
                      if (mStatusLayoutManager.emptyDataLayout != null) mStatusLayoutManager.emptyDataLayout.setView(view);
                      retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);
                      layoutSparseArray.put(id, view);
                      isShow = true;
                  } else {
                      isShow = false;
                  }
                  break;
          }
          return isShow;
      }

6.2 处理重新加载逻辑

  • 最后看看重新加载方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    复制代码/**
    *  重试加载
    */
    private void retryLoad(View view, int id) {
        View retryView = view.findViewById(mStatusLayoutManager.retryViewId != 0 ? mStatusLayoutManager.retryViewId : id);
        if (retryView == null || mStatusLayoutManager.onRetryListener == null) return;
        retryView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mStatusLayoutManager.onRetryListener.onRetry();
            }
        });
    }

07.如何使用该封装库

  • 可以自由切换内容,空数据,异常错误,加载,网络错误等5种状态。父类BaseActivity直接暴露5中状态,方便子类统一管理状态切换,这里fragment的封装和activity差不多。

    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
    复制代码/**
    * ================================================
    * 作    者:杨充
    * 版    本:1.0
    * 创建日期:2017/7/6
    * 描    述:抽取类
    * 修订历史:
    * ================================================
    */
    public abstract class BaseActivity extends AppCompatActivity {

        protected StatusLayoutManager statusLayoutManager;

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_base_view);
            initStatusLayout();
            initBaseView();
            initToolBar();
            initView();
        }

    //子类必须重写该方法
        protected abstract void initStatusLayout();

        protected abstract void initView();

        /**
        * 获取到布局
        */
        private void initBaseView() {
            LinearLayout ll_main = (LinearLayout) findViewById(R.id.ll_main);
            ll_main.addView(statusLayoutManager.getRootLayout());
        }

        //正常展示数据状态
        protected void showContent() {
            statusLayoutManager.showContent();
        }

        //加载数据为空时状态
        protected void showEmptyData() {
            statusLayoutManager.showEmptyData();
        }

        //加载数据错误时状态
        protected void showError() {
            statusLayoutManager.showError();
        }

        //网络错误时状态
        protected void showNetWorkError() {
            statusLayoutManager.showNetWorkError();
        }

        //正在加载中状态
        protected void showLoading() {
            statusLayoutManager.showLoading();
        }
    }
  • 子类继承BaseActivity后,该如何操作呢?具体如下所示

    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
    复制代码@Override
    protected void initStatusLayout() {
    statusLayoutManager = StateLayoutManager.newBuilder(this)
    .contentView(R.layout.activity_main)
    .emptyDataView(R.layout.activity_emptydata)
    .errorView(R.layout.activity_error)
    .loadingView(R.layout.activity_loading)
    .netWorkErrorView(R.layout.activity_networkerror)
    .build();
    }

    //或者添加上监听事件
    @Override
    protected void initStatusLayout() {
        statusLayoutManager = StateLayoutManager.newBuilder(this)
                .contentView(R.layout.activity_content_data)
                .emptyDataView(R.layout.activity_empty_data)
                .errorView(R.layout.activity_error_data)
                .loadingView(R.layout.activity_loading_data)
                .netWorkErrorView(R.layout.activity_networkerror)
                .onRetryListener(new OnRetryListener() {
                    @Override
                    public void onRetry() {
                        //为重试加载按钮的监听事件
                    }
                })
                .onShowHideViewListener(new OnShowHideViewListener() {
                    @Override
                    public void onShowView(View view, int id) {
                        //为状态View显示监听事件
                    }

                    @Override
                    public void onHideView(View view, int id) {
                        //为状态View隐藏监听事件
                    }
                })
                .build();
    }

    //如何切换状态呢?
    showContent();
    showEmptyData();
    showError();
    showLoading();
    showNetWorkError();

    //或者这样操作也可以
    statusLayoutManager.showLoading();
    statusLayoutManager.showContent();
  • 那么如何设置状态页面的交互事件呢?当状态是加载数据失败时,点击可以刷新数据;当状态是无网络时,点击可以设置网络。代码如下所示:

    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
    复制代码/**
    * 点击重新刷新
    */
    private void initErrorDataView() {
        statusLayoutManager.showError();
        LinearLayout ll_error_data = (LinearLayout) findViewById(R.id.ll_error_data);
        ll_error_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                initData();
                adapter.notifyDataSetChanged();
                showContent();
            }
        });
    }

    /**
    * 点击设置网络
    */
    private void initSettingNetwork() {
        statusLayoutManager.showNetWorkError();
        LinearLayout ll_set_network = (LinearLayout) findViewById(R.id.ll_set_network);
        ll_set_network.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("android.settings.WIRELESS_SETTINGS");
                startActivity(intent);
            }
        });
    }
  • 那有些页面想要自定义指定的状态页面UI,又该如何操作呢?倘若有些页面想定制状态布局,也可以自由实现,很简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    复制代码/**
    * 自定义加载数据为空时的状态布局
    */
    private void initEmptyDataView() {
        statusLayoutManager.showEmptyData();
        //此处是自己定义的状态布局
        statusLayoutManager.showLayoutEmptyData(R.layout.activity_emptydata);
        LinearLayout ll_empty_data = (LinearLayout) findViewById(R.id.ll_empty_data);
        ll_empty_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                initData();
                adapter.notifyDataSetChanged();
                showContent();
            }
        });
    }

其他介绍

01.关于博客汇总链接

  • 1.技术博客汇总
  • 2.开源项目汇总
  • 3.生活博客汇总
  • 4.喜马拉雅音频汇总
  • 5.其他汇总

02.关于我的博客

  • github:github.com/yangchong21…
  • 知乎:www.zhihu.com/people/yczb…
  • 简书:www.jianshu.com/u/b7b2c6ed9…
  • csdn:my.csdn.net/m0_37700275
  • 喜马拉雅听书:www.ximalaya.com/zhubo/71989…
  • 开源中国:my.oschina.net/zbj1618/blo…
  • 泡在网上的日子:www.jcodecraeer.com/member/cont…
  • 邮箱:yangchong211@163.com
  • 阿里云博客:yq.aliyun.com/users/artic… 239.headeruserinfo.3.dT4bcV
  • segmentfault头条:segmentfault.com/u/xiangjian…
  • 掘金:juejin.cn/user/197877…

项目开源地址:github.com/yangchong21…

本文转载自: 掘金

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

微信支付开发实记

发表于 2019-07-17

微信支付分为JSAPI支付,扫码支付,APP支付,小程序支付等不同的支付方式。但大体的支付过程是一致的,本文以JSAPI支付,也就是微信内的H5支付为例,描述一下支付的整个开发流程。

配置

商户需要提前开通商户平台,并去公众平台或开放平台提交微信支付申请,获得商户号和秘钥。

详细文档可以看这里

支付流程

微信内网页支付时序图
微信支付的流程图画的很完整,开发前要把整个流程研究清楚。

整个流程,服务端需要做的有三件事。

  1. 前端支付按钮被触发后,服务端要去调用 统一下单 接口,把预付单信息、支付参数和参数签名返回给前端。前端根据这些参数唤起支付。
  2. 当用户支付成功后,微信会给我们一个回调通知,告知我们支付结果。这一步要实现“完成订单”操作,标记用户已经成功支付,进入“发货”流程。
  3. 提供一个查询接口,让前端再次确认是否支付成功。

统一下单

在支付前,商户系统先调用该接口在微信支付后台生成预支付交易单,同样的,商户系统也需要在自己的表里记录一笔“未完成订单”。生成之后返回正确的预付单信息、支付参数和参数签名返回给前端。前端根据这些参数唤起支付。

接口

https://api.mch.weixin.qq.com/pay/unifiedorder
参数巨多,具体还是看文档

这里需要说明的一点是,我们在调用这个接口时,需要签一次名用来给微信做校验,微信也返回了一个新的签名用来给我们做校验,然后我们还要返回给前端一个签名,用来唤起支付。这三个签名都不是同一个。

我们不能直接把调用统一下单接口返回的签名返回给前端,而是根据前端唤起支付的参数去重新签名。

注意,是根据前端唤起支付的参数去重新签名,因为前端的参数名和后端的参数名会略微有差别,这里需要小心。

说明:签名的意图是用来校验身份,当前端把这些参数传给微信,微信会把调用参数除去签名后重新签名,用来校验签名的正确性,所以用来签名的参数名要和前端参数一致。

支付结果通知

在统一下单时我们填了一个参数叫 notify_url,这是一个服务端的接口地址,微信在用户支付成功后,会回调这个地址,告知我们支付结果。

详情看文档

在这一步还是需要做多点校验的,免得被人有机可乘。

  1. 校验支付是否成功,不成功直接返回”FAIL”
  2. 校验签名和appid
  3. 校验订单是否完成(幂等校验,防止微信多次回调导致多次订单写入)
  4. 订单金额校验

一通校验完事之后就可以做业务相关的事了。记得所有操作结束后返回”SUCCESS”,不然微信会不断发起回调。

总结

  1. 流程图描述的很清楚,要仔细阅读流程图。
  2. 调用完下单接口后要进行二次签名,签名的参数要看前端验签用哪些参数,即使是同一个参数,字段名也会跟第一次加签不一样。
  3. 前端支付完成之后微信会有一个回调,我们需要做以下几点校验:
    1. 做幂等处理(因为同样的通知微信可能发送多次)。
    2. 校验签名,校验APPID。
    3. 校验订单金额。
  4. 订单状态分为 0-未支付 1-支付完成 2-支付失败:
    1. 用户触发支付组件然后关闭或者杀掉进程微信不会给到后台任何回复,始终处于 0-未支付,所以这个状态也是一个支付失败状态。
    2. 支付失败比较少见(到现在没有遇到过),比如签名错误(发生在调试阶段)。
  5. 微信回执表尽量详细的记录微信传回的所有必有参数,以备出问题时排查,可以直接丢到mongodb里。

本文转载自: 掘金

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

谈谈 redis 在项目中的常见使用场景

发表于 2019-07-17

最近在写一个脚手架,其中 redis 的使用场景还挺多,于是总结下它的常见使用场景

  • 本文链接: shanyue.tech/post/redis-…
  • github 备份: github.com/shfshanyue/…

缓存

1
2
3
4
复制代码> set User:1:name shanyue EX 100 NX
OK
> get User:1:name
"shanyue"

缓存是 redis 出镜率最高的一种使用场景,仅仅使用 set/get 就可以实现,不过也有一些需要考虑的点

  • 如何更好地设置缓存
  • 如何保持缓存与上游数据的一致性
  • 如何解决缓存血崩,缓存击穿问题

session: 用户登录及验证码

1
2
3
4
复制代码> set 5d27e60e6fb9a07f03576687 '{"id": 10086, role: "ADMIN"}' EX 7200
OK
> get 5d27e60e6fb9a07f03576687
"{\"id\": 10086, role: \"ADMIN\"}"

这也是很常用的一种场景,不过相对于有状态的 session,也可以考虑使用 JWT,各有利弊

  • json web token 实践登录以及校验码验证

消息队列

1
2
3
4
5
6
复制代码> lpush UserEmailQueue 1 2 3 4
lpop UserEmailQueue
> rpop UserEmailQueue
1
> rpop UserEmailQueue
2

可以把 redis 的队列视为分布式队列,作为消息队列时,生产者在一头塞数据,消费者在另一头出数据: (lpush/rpop, rpush/lpop)。不过也有一些不足,而这些不足有可能是致命的,不过对于一些丢几条消息也没关系的场景还是可以考虑的

  1. 没有 ack,有可能丢消息
  2. 需要做 redis 的持久化配置

过滤器 (dupefilter)

1
2
3
4
5
6
7
8
9
复制代码> sadd UrlSet http://1
(integer) 1
> sadd UrlSet http://2
(integer) 1
> sadd UrlSet http://2
(integer) 0
> smembers UrlSet
1) "http://1"
2) "http://2"

scrapy-redis 作为分布式的爬虫框架,便是使用了 redis 的 Set 这个数据结构来对将要爬取的 url 进行去重处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码# https://github.com/rmax/scrapy-redis/blob/master/src/scrapy_redis/dupefilter.py
def request_seen(self, request):
"""Returns True if request was already seen.
Parameters
----------
request : scrapy.http.Request
Returns
-------
bool
"""
fp = self.request_fingerprint(request)
added = self.server.sadd(self.key, fp)
return added == 0

不过当 url 过多时,会有内存占用过大的问题

分布式锁

1
2
3
4
5
6
7
8
复制代码set Lock:User:10086 06be97fc-f258-4202-b60b-8d5412dd5605 EX 60 NX

# 释放锁,一段 LUA 脚本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

这是一个最简单的单机版的分布式锁,有以下要点

  • EX 表示锁会过期释放
  • NX 保证原子性
  • 解锁时对比资源对应产生的 UUID,避免误解锁

当你使用分布式锁是为了解决一些性能问题,如分布式定时任务防止执行多次 (做好幂等性),而且鉴于单点 redis 挂掉的可能性很小,可以使用这种单机版的分布式锁。

Rate Limit

限流即在单位时间内只允许通过特定数量的请求,有两个关键参数

  • window,单位时间
  • max,最大请求数量

最常见的场景: 短信验证码一分钟只能发送两次

1
2
3
4
5
6
7
8
9
10
11
复制代码FUNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
ERROR "too many requests per second"
ELSE
value = INCR(ip)
IF value == 1 THEN
EXPIRE(ip,1)
END
PERFORM_API_CALL()
END

可以使用计数器对 API 的请求进行限流处理,但是要注意几个问题

  1. 在平滑的滑动窗口时间内在极限情况下会有两倍数量的请求数
  2. 条件竞争 (Race Condition)

这时候可以通过编程,根据 TTL key 进行进一步限制,或者使用一个 LIST 来维护每次请求打来的时间戳进行实时过滤。以下是 node 实现的一个 Rate Limter。参考源码 node-rate-limiter-flexible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码this.client
.multi()
.set(rlKey, 0, 'EX', secDuration, 'NX')
.incrby(rlKey, points)
.pttl(rlKey)
.exec((err, res) => {
if (err) {
return reject(err);
}

return resolve(res);
})

if (res.consumedPoints > this.points) {
// ...
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
// ...
setTimeout(resolve, delay, res);
} else {
resolve(res);
}
  • node-rate-limiter-flexible
  • 邮件发送,限流,漏桶与令牌桶

分布式 websocket

可以通过 redis 的 PUB/SUB 来在 websocket server 间进行交流。可以参考以下项目

  • socket.io-redis

欢迎关注我的公众号山月行,在这里记录着我的技术成长,欢迎交流

欢迎关注公众号山月行,在这里记录我的技术成长,欢迎交流

本文转载自: 掘金

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

阿里面试官都爱问的内存管理和GC算法及回收策略

发表于 2019-07-17

JVM内存组成结构

JVM栈由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示:

JVM内存回收

Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)

1.Young(年轻代)

年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。

2.Tenured(年老代)

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

3.Perm(持久代)

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。

通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。

关于JVM内存管理的一些建议

  1. 手动将生成的无用对象,中间对象置为null,加快内存回收。
  2. 对象池技术如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池来较少对象的生成。如果有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提高了对象的复用率。
  3. JVM调优通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证JVM内存回收时,可以考虑采用JVM调优的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。

垃圾对象的判定

Java堆中存放着几乎所有的对象实例,垃圾收集器对堆中的对象进行回收前,要先确定这些对象是否还有用,判定对象是否为垃圾对象有如下算法:

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器都为0的对象就是不可能再被使用的。

引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择,当Java语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。

根搜索算法

**Java和C#**中都是采用根搜索算法来判定对象是否存活的。这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。在Java语言里,可作为GC Roots的兑现包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(Native方法)的引用对象。

实际上,在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

垃圾收集算法

判定除了垃圾对象之后,便可以进行垃圾回收了。下面介绍一些垃圾收集算法,由于垃圾收集算法的实现涉及大量的程序细节,因此这里主要是阐明各算法的实现思想,而不去细论算法的具体实现。

标记—清除算法

标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:

该算法有如下缺点:

  • 标记和清除过程的效率都不高。
  • 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
复制算法

复制算法比较适合于新生代,复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:

  • 每次只对一块内存进行回收,运行高效。
  • 只需移动栈顶指针,按顺序分配内存即可,实现简单。
  • 内存回收时不用考虑内存碎片的出现。

它的缺点是:可一次性分配的最大内存缩小了一半。
复制算法的执行情况如下图所示:

但一般不用按1:1划分内存空间,可以分成一个大的eden和两块小的survivor。

标记—整理算法

在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:

分代收集

当前商业虚拟机的垃圾收集都采用分代收集来管理内存,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

每个对象都有一个年龄(Age)计数器,如果对象在Eden出声并讲过一次Minor GC还存活,将被移动到Survivor区并将Age设置为1,之后每在Survivor区中熬过一次Minor GC,Age就加1,当增加到一定程度(默认为15),就可以放到老年代中。

垃圾收集器

垃圾收集器是内存回收算法的具体实现,Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。Sun HotSpot虚拟机1.6版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。这些收集器以不同的组合形式配合工作来完成不同分代区的垃圾收集工作。

垃圾回收分析

在用代码分析之前,我们对内存的分配策略明确以下三点:

  • 对象优先在Eden分配。当Eden没有足够空间分配时,将发起一次Minor GC
  • 大对象(需要大量连续空间的java对象,如长的字符串和数组)直接进入老年代。由于新生代使用复制算法回收内存,这样可以避免在Eden和两个Survivor区之间发生大量的内存复制。
  • 长期存活的对象将进入老年代。

对垃圾回收策略说明以下两点:

  • 新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。

Dalvik虚拟机使用Mark-Sweep算法来进行垃圾收集。顾名思义,Mark-Sweep算法就是为Mark和Sweep两个阶段进行垃圾回收。其中,Mark阶段从根集(Root Set)开始,递归地标记出当前所有被引用的对象,而Sweep阶段负责回收那些没有被引用的对象。在分析Dalvik虚拟机使用的Mark-Sweep算法之前,我们先来了解一下什么情况下会触发GC。

读者福利、完整面试题【含答案】Java核心笔记,Java架构面试专题整合千道(pdf文档)

本文转载自: 掘金

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

Mysql数据库监听binlog

发表于 2019-07-15

前言

我们经常需要根据用户对自己数据的一些操作来做一些事情.

比如如果用户删除了自己的账号,我们就给他发短信骂他,去发短信求他回来.

类似于这种功能,当然可以在业务逻辑层实现,在收到用户的删除请求之后执行这一操作,但是数据库的binlog为我们提供了另外一种操作方法.

要监听binlog,需要两步,第一步当然是你的mysql需要开启这一个功能,第二个是要写程序来对日志进行读取.

mysql开启binlog.

首先mysql的binlog日常是不打开的,因此我们需要:

  1. 找到mysql的配置文件my.cnf,这个因操作系统不一样,位置也不一定一样,可以自己找一下,
  2. 在其中加入以下内容:
1
2
3
4
5
复制代码
[mysqld]
server_id = 1
log-bin = mysql-bin
binlog-format = ROW
  1. 之后重启mysql.
1
2
3
4
复制代码/ ubuntu
service mysql restart
// mac
mysql.server restart
  1. 监测是否开启成功

进入mysql命令行,执行:

1
2
复制代码
show variables like '%log_bin%' ;

如果结果如下图,则说明成功了:

2019-04-29-00-31-29

  1. 查看正在写入的binlog状态:

2019-04-29-00-32-14

代码读取binlog

引入依赖

我们使用开源的一些实现,这里因为一些奇怪的原因,我选用了mysql-binlog-connector-java这个包,(官方github仓库)[github.com/shyiko/mysq…]具体依赖如下:

1
2
3
4
5
6
复制代码<!-- https://mvnrepository.com/artifact/com.github.shyiko/mysql-binlog-connector-java -->
<dependency>
<groupId>com.github.shyiko</groupId>
<artifactId>mysql-binlog-connector-java</artifactId>
<version>0.17.0</version>
</dependency>

当然,对binlog的处理有很多开源实现,阿里的cancl就是一个,也可以使用它.

写个demo

根据官方仓库中readme里面,来简单的写个demo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码    public static void main(String[] args) {
BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "passwd");
EventDeserializer eventDeserializer = new EventDeserializer();
eventDeserializer.setCompatibilityMode(
EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG,
EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY
);
client.setEventDeserializer(eventDeserializer);
client.registerEventListener(new BinaryLogClient.EventListener() {

@Override
public void onEvent(Event event) {
// TODO
dosomething();
logger.info(event.toString());
}
});
client.connect();
}

这个完全是根据官方教程里面写的,在onEvent里面可以写自己的业务逻辑,由于我只是测试,所以我在里面将每一个event都打印了出来.

之后我手动登录到mysql,分别进行了增加,修改,删除操作,监听到的log如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码00:23:13.331 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=0, eventType=ROTATE, serverId=1, headerLength=19, dataLength=28, nextPosition=0, flags=32}, data=RotateEventData{binlogFilename='mysql-bin.000001', binlogPosition=886}}
00:23:13.334 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468403000, eventType=FORMAT_DESCRIPTION, serverId=1, headerLength=19, dataLength=100, nextPosition=0, flags=0}, data=FormatDescriptionEventData{binlogVersion=4, serverVersion='5.7.23-0ubuntu0.16.04.1-log', headerLength=19, dataLength=95}}
00:23:23.715 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468603000, eventType=ANONYMOUS_GTID, serverId=1, headerLength=19, dataLength=46, nextPosition=951, flags=0}, data=null}
00:23:23.716 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468603000, eventType=QUERY, serverId=1, headerLength=19, dataLength=51, nextPosition=1021, flags=8}, data=QueryEventData{threadId=4, executionTime=0, errorCode=0, database='pf', sql='BEGIN'}}
00:23:23.721 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468603000, eventType=TABLE_MAP, serverId=1, headerLength=19, dataLength=32, nextPosition=1072, flags=0}, data=TableMapEventData{tableId=108, database='pf', table='student', columnTypes=15, 3, columnMetadata=135, 0, columnNullability={}}}
00:23:23.724 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468603000, eventType=EXT_WRITE_ROWS, serverId=1, headerLength=19, dataLength=23, nextPosition=1114, flags=0}, data=WriteRowsEventData{tableId=108, includedColumns={0, 1}, rows=[
[[B@546a03af, 2]
]}}
00:23:23.725 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468603000, eventType=XID, serverId=1, headerLength=19, dataLength=12, nextPosition=1145, flags=0}, data=XidEventData{xid=28}}
00:23:55.872 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468635000, eventType=ANONYMOUS_GTID, serverId=1, headerLength=19, dataLength=46, nextPosition=1210, flags=0}, data=null}
00:23:55.872 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468635000, eventType=QUERY, serverId=1, headerLength=19, dataLength=51, nextPosition=1280, flags=8}, data=QueryEventData{threadId=4, executionTime=0, errorCode=0, database='pf', sql='BEGIN'}}
00:23:55.873 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468635000, eventType=TABLE_MAP, serverId=1, headerLength=19, dataLength=32, nextPosition=1331, flags=0}, data=TableMapEventData{tableId=108, database='pf', table='student', columnTypes=15, 3, columnMetadata=135, 0, columnNullability={}}}
00:23:55.875 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468635000, eventType=EXT_UPDATE_ROWS, serverId=1, headerLength=19, dataLength=31, nextPosition=1381, flags=0}, data=UpdateRowsEventData{tableId=108, includedColumnsBeforeUpdate={0, 1}, includedColumns={0, 1}, rows=[
{before=[[B@6833ce2c, 1], after=[[B@725bef66, 3]}
]}}
00:23:55.875 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468635000, eventType=XID, serverId=1, headerLength=19, dataLength=12, nextPosition=1412, flags=0}, data=XidEventData{xid=41}}
00:24:22.333 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468662000, eventType=ANONYMOUS_GTID, serverId=1, headerLength=19, dataLength=46, nextPosition=1477, flags=0}, data=null}
00:24:22.334 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468662000, eventType=QUERY, serverId=1, headerLength=19, dataLength=51, nextPosition=1547, flags=8}, data=QueryEventData{threadId=4, executionTime=0, errorCode=0, database='pf', sql='BEGIN'}}
00:24:22.334 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468662000, eventType=TABLE_MAP, serverId=1, headerLength=19, dataLength=32, nextPosition=1598, flags=0}, data=TableMapEventData{tableId=108, database='pf', table='student', columnTypes=15, 3, columnMetadata=135, 0, columnNullability={}}}
00:24:22.335 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468662000, eventType=EXT_DELETE_ROWS, serverId=1, headerLength=19, dataLength=23, nextPosition=1640, flags=0}, data=DeleteRowsEventData{tableId=108, includedColumns={0, 1}, rows=[
[[B@1888ff2c, 3]
]}}
00:24:22.335 [main] INFO util.MysqlBinLog - Event{header=EventHeaderV4{timestamp=1556468662000, eventType=XID, serverId=1, headerLength=19, dataLength=12, nextPosition=1671, flags=0}, data=XidEventData{xid=42}}

根据自己的业务,封装一个更好使,更定制的工具类

开始的时候打算贴代码的,,,但是代码越写越多,索性传在github上了,这里只贴部分的实现.代码传送门

实现思路

  1. 支持对单个表的监听,因为我们不想真的对所有数据库中的所有数据表进行监听.
  2. 可以多线程消费.
  3. 把监听到的内容转换成我们喜闻乐见的形式(文中的数据结构不一定很好,我没想到更加合适的了).

所以实现思路大致如下:

  1. 封装个客户端,对外只提供获取方法,屏蔽掉初始化的细节代码.
  2. 提供注册监听器(伪)的方法,可以注册对某个表的监听(重新定义一个监听接口,所有注册的监听器实现这个就好).
  3. 真正的监听器只有客户端,他将此数据库实例上的所有操作,全部监听到并转换成我们想要的格式LogItem放进阻塞队列里面.
  4. 启动多个线程,消费阻塞队列,对某一个LogItem调用对应的数据表的监听器,做一些业务逻辑.

初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码    public MysqlBinLogListener(Conf conf) {
BinaryLogClient client = new BinaryLogClient(conf.host, conf.port, conf.username, conf.passwd);
EventDeserializer eventDeserializer = new EventDeserializer();
eventDeserializer.setCompatibilityMode(
EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG,
EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY
);
client.setEventDeserializer(eventDeserializer);
this.parseClient = client;
this.queue = new ArrayBlockingQueue<>(1024);
this.conf = conf;
listeners = new ConcurrentHashMap<>();
dbTableCols = new ConcurrentHashMap<>();
this.consumer = Executors.newFixedThreadPool(consumerThreads);
}

注册代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码    public void regListener(String db, String table, BinLogListener listener) throws Exception {
String dbTable = getdbTable(db, table);
Class.forName("com.mysql.jdbc.Driver");
// 保存当前注册的表的colum信息
Connection connection = DriverManager.getConnection("jdbc:mysql://" + conf.host + ":" + conf.port, conf.username, conf.passwd);
Map<String, Colum> cols = getColMap(connection, db, table);
dbTableCols.put(dbTable, cols);

// 保存当前注册的listener
List<BinLogListener> list = listeners.getOrDefault(dbTable, new ArrayList<>());
list.add(listener);
listeners.put(dbTable, list);
}

在这个步骤中,我们在注册监听者的同时,获得了该表的schema信息,并保存到map里面去,方便后续对数据进行处理.

监听代码:

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
复制代码    @Override
public void onEvent(Event event) {
EventType eventType = event.getHeader().getEventType();

if (eventType == EventType.TABLE_MAP) {
TableMapEventData tableData = event.getData();
String db = tableData.getDatabase();
String table = tableData.getTable();
dbTable = getdbTable(db, table);
}

// 只处理添加删除更新三种操作
if (isWrite(eventType) || isUpdate(eventType) || isDelete(eventType)) {
if (isWrite(eventType)) {
WriteRowsEventData data = event.getData();
for (Serializable[] row : data.getRows()) {
if (dbTableCols.containsKey(dbTable)) {
LogItem e = LogItem.itemFromInsert(row, dbTableCols.get(dbTable));
e.setDbTable(dbTable);
queue.add(e);
}
}
}
}
}

我偷懒了,,,这里面只实现了对添加操作的处理,其他操作没有写.

消费代码:

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
复制代码
public void parse() throws IOException {
parseClient.registerEventListener(this);

for (int i = 0; i < consumerThreads; i++) {
consumer.submit(() -> {
while (true) {
if (queue.size() > 0) {
try {
LogItem item = queue.take();
String dbtable = item.getDbTable();
listeners.get(dbtable).forEach(l -> {
l.onEvent(item);
});

} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread.sleep(1000);
}
});
}
parseClient.connect();
}

消费时,从队列中获取item,之后获取对应的一个或者多个监听者,分别消费这个item.

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码    public static void main(String[] args) throws Exception {
Conf conf = new Conf();
conf.host = "hostname";
conf.port = 3306;
conf.username = conf.passwd = "hhsgsb";

MysqlBinLogListener mysqlBinLogListener = new MysqlBinLogListener(conf);
mysqlBinLogListener.parseArgsAndRun(args);
mysqlBinLogListener.regListener("pf", "student", item -> {
System.out.println(new String((byte[])item.getAfter().get("name")));
logger.info("insert into {}, value = {}", item.getDbTable(), item.getAfter());
});
mysqlBinLogListener.regListener("pf", "teacher", item -> System.out.println("teacher ===="));

mysqlBinLogListener.parse();
}

在这段很少的代码里,注册了两个监听者,分别监听student和teacher表,并分别进行打印处理,经测试,在teacher表插入数据时,可以独立的运行定义的业务逻辑.

注意:这里的工具类并不能直接投入使用,因为里面有许多的异常处理没有做,且功能仅监听了插入语句,可以用来做实现的参考.

参考文章

github.com/shyiko/mysq…

cloud.tencent.com/developer/a…

ChangeLog

2019-04-30 完
2019-05-01 使用Multimap替换Map>

以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客——>呼延十

本文转载自: 掘金

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

数据结构之栈——算术表达式求值

发表于 2019-07-14

定义

栈是一种特殊的线性表,它只能在一端进行插入或者删除操作,能进行操作的一端称为栈顶,另一端则称为栈底。也由于这个特性,导致先进入的元素,只能后出,因此栈是后进先出的线性表。

栈是一种线性表,因此它的存储可以是链式存储,也可以是顺序存储。链式存储的栈,称为链栈,顺序存储的栈,称为顺序栈。而下面实例中,使用go语言的可变数组slice完成栈的存储,因此是顺序存储。

栈的描述

1
2
3
4
5
6
7
8
9
10
11
复制代码type data interface {}

type Stack struct {
top int // 栈顶,代表栈顶的下标,-1为空栈
list []data // 从0开始存储
}

func New() *Stack{
s := &Stack{top:-1,list: []data{}}
return s
}

主要操作

入栈

在栈顶插入一个新元素,称为入栈,首先要判断栈是否已满,满就报错,否则就插入,并将栈顶上升一位。

1
2
3
4
5
复制代码// 由于使用的是可变数组,因此不会出现栈满的情况,所以以下代码不需要判断是否栈满,如果是不可变数组,或者是限制容量的栈,则要判断栈是否已满
func (s *Stack) Push (value data) {
s.list = append(s.list,value)
s.top++
}

出栈

取出栈顶元素,并将栈顶下降一位,如果栈为空,则报错

1
2
3
4
5
6
7
8
9
复制代码func (s *Stack) Pop() (data,error) {
if s.top == -1 {
return nil,errors.New("栈空")
}
data := s.list[s.top]
s.list = s.list[:s.top]
s.top--
return data,nil
}

判空

判断是否为空栈

1
2
3
复制代码func (s *Stack) IsEmpty() bool{
return s.top == -1
}

获取栈顶元素

得到栈顶元素的值,但不出栈

1
2
3
4
5
6
7
复制代码func (s *Stack) GetTop() data{
if s.top == -1 {
return nil
}
return s.list[s.top]
}
}

应用:算术表达式求值

后缀表达式

我们日常生活中使用的算术表达式,例如:5+6/2-3*4,它由两类对象构成:

  • 运算数,如:5,6,2等
  • 运算符号,如+,-等,而且不同运算符号优先级不一样

由于运算符号优先级不同,而且运算符号位于运算数中,所以使得运算变得复杂了。
怎么理解呢?拿上面的表达式为例,当程序遍历到+号的时候,要做+运算,那么是5+6吗?很显然不是,因为6后面还有一个/运算符,而且/的优先级大于+,所以6是/的运算数,而不是+的运算数。那么反过来呢?当遍历到一个运算符号的时候,就已经知道对应的两个运算数,这样求值就变的简单了。伟大的科学家们,就此发明了后缀表达式,也称逆波兰式,指的是不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则)。

  • 中缀表达式:2 + 9 / 3 - 5
  • 后缀表达式:2 9 3 / + 5 -

当然两个表达式,都是表达同一个意思。

后缀表达式求值

由于后缀表达式不需要考虑运算符的优先规则,因此求值算法就变得简单了:

1、从左到右依次遍历表达式;

2、遇到数字就直接入栈;

3、遇到操作符就弹出两个元素,先弹出的元素放到操作符的右边,后弹出的元素放到操作符的左边(左边的运算数先入栈,因此后出),将计算得到的结果再压入栈;

4、直到整个表达式遍历完,最后弹出的栈顶元素就是表达式最终的值。

以33-5+表达式为例,运行状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码func getPostfixExpressionResult(s string) int {
stack := Stack.New()
for _, value := range s {
if unicode.IsDigit(value) {
intValue, _ := strconv.Atoi(string(value))
stack.Push(intValue)
} else {
right, _ := stack.Pop()
left, _ := stack.Pop()
result := calculate(left.(int), right.(int), string(value)) // 封装的 + - * / 求值方法
stack.Push(result)
}
}
result, _ := stack.Pop()
return result.(int)
}

getPostfixExpressionResult("33-5+") // 5

可以看得出算法时间复杂度为O(n),并且通过出栈入栈的形式就可以完成求值过程。

中缀表达式转后缀表达式

接下来看看中缀怎么转后缀,我们先对比一下两个表达式:

  • 中缀表达式:2 + 9 / 3 - 5
  • 后缀表达式:2 9 3 / + 5 -

可以看的出来,数字的相对位置是不变的,改变的是符号的位置,那么在转换的过程,我们需要对比各种运算符号的优先级,然后将优先级高的运算符,先输出,低的后输出,这样在后缀表达式求值的时候,就能保存计算顺序不被改变。(左括号和右括号也看成运算符号)具体的算法步骤如下:

1、从左到右遍历中缀表达式;

2、如果是运算数,直接输出;

3、如果是运算符号:若优先级大于栈顶的运算符号(栈不为空),则将该运算符号压入栈中,因为如果该运算符号优先级比栈顶的大,说明要先被计算,那么它是后入的,因此在之后的操作中,一定比栈顶的符号先出,因此在后缀求值中,肯定先被计算;

4、如果是运算符号:若优先级小于等于栈顶的运算符号(栈不为空),则将栈顶的运算符号弹出并输出,然后继续和下一个新的栈顶元素对比,直到优先级大于新的栈顶元素,就将该运算符号压入栈中;

5、左括号:括号里的表达式肯定是要先计算的,因此当扫描到左括号的时候,直接将左括号压入栈中,而入了栈里的左括号,优先级就变到最低了。因为括号里的运算符要先运算;

6、右括号:将栈顶元素弹出并输入,直到遇到左括号(弹出,但不输出);

7、整个表达式遍历完之后,则将栈里的元素一一弹出并输出。

我们以2*(9+6/3-2)+4为例:

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
复制代码// 利用hash的形式来判断运算符号的优先级
// 有兴趣可以看看百度百科里(后缀表达式)是怎么判断运算符号优先级的,很有意思 (>▽<)
var opPriority = map[string]int{
"*":2,
"/":2,
"+":1,
"-":1,
"(":0,
}

func infixToPostfix(s string) string{
postfix := ""
stack := Stack.New()
for _,value :=range s{
if unicode.IsDigit(value) {
postfix += string(value)
} else {
op := string(value)
switch op{
case "+","-","*","/":
if stack.IsEmpty(){
stack.Push(op)
} else {
pop := stack.GetTop()
for opPriority[op] <= opPriority[pop.(string)] {
pop,_ = stack.Pop()
postfix += pop.(string)
if stack.IsEmpty() {
break
}
pop = stack.GetTop()
}
stack.Push(op)
}
case "(":
stack.Push(op)
case ")":
for !stack.IsEmpty() {
op,_ := stack.Pop()
if op.(string) == "(" {
break
}
postfix +=op.(string)
}
}
}
}

for !stack.IsEmpty(){
op,_ := stack.Pop()
postfix +=op.(string)
}
return postfix
}

infixToPostfix("2*(9+6/3-5)+4") // 2963/+5-*4+

总结

栈是一种被广泛应用的数据结构,除了刚刚举例的算术表达式求值之外,栈还用于函数调用及递归实现,回溯算法等等。在适当的时候选择栈,可以更加高效的解决问题。

ps:在上面的例子里,后缀表达式求值的程序,是一个不太正确的程序,准确的讲,它会把每个数字看成运算数,即122*,它不会计算出24,而是变成了4,估计大家也猜到原因了,因为程序是一个字符一个字符的遍历,而没有位数的划分,修正的话,可以用数组来存表达式,这样就可以正确的划分运算数。

Thanks!

本文转载自: 掘金

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

1…864865866…956

开发者博客

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