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

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


  • 首页

  • 归档

  • 搜索

我为何弃用Jetpack的App Startup?

发表于 2020-08-11

前言

最近Jetpack又添加了新成员App Startup,官方声明这是一个在Android应用启动时,针对初始化组件进行优化的依赖库。本人第一次听到后非常高兴,因为自己负责的项目在启动时需要初始化的东西实在是太多,而且有点杂乱无章,都耦合在一起了。对于可以异步初始化的组件也没有进行异步处理,而对于已经处理过的异步组件它们之间的依赖关系或者多个异步之后的统一逻辑处理也没有一个很好的统一规范。所以针对这种情况早就想找个方案来优化了,这次终于等到了App Startup。

但是,当我元气满满的去查看官方文档时,并没有找到预想中的结果。官方文档中只提到了可以通过一个ContentProvider来统一管理需要初始化的组件,同时通过dependencies()方法解决组件间初始化的依赖顺序,然后呢?没了?等等官方你是不是漏了什么?

异步处理呢?虽然我们可以在create()方法中手动创建子线程进行异步任务,但一个异步任务依赖另一个异步任务又该如何处理呢?多个异步任务完成之后,统一逻辑处理又在哪里呢?依赖任务完成后的回调又在哪里?亦或者是依赖任务完成后的通知?

我有点不相信,所以又去查看了App Startup的源码,源码很简单,也就几个文件,最后发现确实只支持上面的那几个功能。

如果你的项目都是同步初始化的话,并且使用到了多个ContentProvider,App Startup可能有一定的优化空间,毕竟统一到了一个ContentProvider中,同时支持了简单的顺序依赖。

值得一提的是,App Startup中只提供了使用反射来获取初始化的组件实例,这对于一些没有过多依赖的初始化项目来说,盲目使用App Startup来优化是否会对启动速度进一步造成影响呢?

所以细想了一下,不禁让我想起了三国时的一个名词:鸡肋。食之无味,弃之可惜。

但最终我还是决定放弃使用它。

放弃之后有点不甘心,可能更多的是它没有解决我当前的项目场景。都分析了这么多,源码都看了,总不能半途而废吧,所以自己咬咬牙再补充一点呗。

所以坚持一下,就有了下面这个库,App Startup的进阶版Android Startup。

Android Startup

Android Startup提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用Android Startup来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。
与此同时,Android Startup支持同步与异步等待,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。

由于Android Startup是基于App Startup进行的扩展,所以它的使用方式与App Startup有点类似,该有的功能基本上都有,同时额外还附加其它功能。

下面是一张与google的App Startup功能对比的表格。

指标 App Startup Android Startup
手动配置 ✅ ✅
自动配置 ✅ ✅
依赖支持 ✅ ✅
闭环处理 ✅ ✅
线程控制 ❌ ✅
异步等待 ❌ ✅
依赖回调 ❌ ✅
拓扑优化 ❌ ✅

下面简单介绍一下Android Startup的使用。

添加依赖

将下面的依赖添加到build.gradle文件中:

1
2
3
arduino复制代码dependencies {
implementation 'com.rousetime.android:android-startup:1.0.1'
}

快速使用

android-startup提供了两种使用方式,在使用之前需要先定义初始化的组件。

定义初始化的组件

每一个初始化的组件都需要实现AndroidStartup<T>抽象类,它实现了Startup<T>接口,它主要有以下四个抽象方法:

  • callCreateOnMainThread(): Boolean用来控制create()方法调时所在的线程,返回true代表在主线程执行。
  • waitOnMainThread(): Boolean用来控制当前初始化的组件是否需要在主线程进行等待其完成。如果返回true,将在主线程等待,并且阻塞主线程。
  • create(): T?组件初始化方法,执行需要处理的初始化逻辑,支持返回一个T类型的实例。
  • dependencies(): List<Class<out Startup<*>>>?返回Startup<*>类型的list集合。用来表示当前组件在执行之前需要依赖的组件。

例如,下面定义一个SampleFirstStartup类来实现AndroidStartup<String>抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码class SampleFirstStartup : AndroidStartup<String>() {

override fun callCreateOnMainThread(): Boolean = true

override fun waitOnMainThread(): Boolean = false

override fun create(context: Context): String? {
// todo something
return this.javaClass.simpleName
}

override fun dependencies(): List<Class<out Startup<*>>>? {
return null
}

}

因为SampleFirstStartup在执行之前不需要依赖其它组件,所以它的dependencies()方法可以返回空,同时它会在主线程中执行。

注意:️虽然waitOnMainThread()返回了false,但由于它是在主线程中执行,而主线程默认是阻塞的,所以callCreateOnMainThread()返回true时,该方法设置将失效。

假设你还需要定义SampleSecondStartup,它依赖于SampleFirstStartup。这意味着在执行SampleSecondStartup之前SampleFirstStartup必须先执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码class SampleSecondStartup : AndroidStartup<Boolean>() {

override fun callCreateOnMainThread(): Boolean = false

override fun waitOnMainThread(): Boolean = true

override fun create(context: Context): Boolean {
// 模仿执行耗时
Thread.sleep(5000)
return true
}

override fun dependencies(): List<Class<out Startup<*>>>? {
return listOf(SampleFirstStartup::class.java)
}

}

在dependencies()方法中返回了SampleFirstStartup,所以它能保证SampleFirstStartup优先执行完毕。
它会在子线程中执行,但由于waitOnMainThread()返回了true,所以主线程会阻塞等待直到它执行完毕。

例如,你还定义了SampleThirdStartup与SampleFourthStartup

Manifest中自动配置

第一种初始化方法是在Manifest中进行自动配置。

在Android Startup中提供了StartupProvider类,它是一个特殊的content provider,提供自动识别在manifest中配置的初始化组件。
为了让其能够自动识别,需要在StartupProvider中定义<meta-data>标签。其中的name为定义的组件类,value的值对应为android.startup。

1
2
3
4
5
6
7
8
9
10
ini复制代码<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${applicationId}.android_startup"
android:exported="false">

<meta-data
android:name="com.rousetime.sample.startup.SampleFourthStartup"
android:value="android.startup" />

</provider>

你不需要将SampleFirstStartup、SampleSecondStartup与SampleThirdStartup添加到<meta-data>标签中。这是因为在SampleFourthStartup中,它的dependencies()中依赖了这些组件。StartupProvider会自动识别已经声明的组件中依赖的其它组件。

Application中手动配置

第二种初始化方法是在Application进行手动配置。

手动初始化需要使用到StartupManager.Builder()。

例如,如下代码使用StartupManager.Builder()进行初始化配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码class SampleApplication : Application() {

override fun onCreate() {
super.onCreate()
StartupManager.Builder()
.addStartup(SampleFirstStartup())
.addStartup(SampleSecondStartup())
.addStartup(SampleThirdStartup())
.addStartup(SampleFourthStartup())
.build(this)
.start()
.await()
}
}

如果你开启了日志输出,然后运行项目之后,将会在控制台中输出经过拓扑排序优化之后的初始化组件的执行顺序。

1
2
3
4
5
6
7
yaml复制代码 D/StartupTrack: TopologySort result: 
================================================ ordering start ================================================
order [0] Class: SampleFirstStartup => Dependencies size: 0 => callCreateOnMainThread: true => waitOnMainThread: false
order [1] Class: SampleSecondStartup => Dependencies size: 1 => callCreateOnMainThread: false => waitOnMainThread: true
order [2] Class: SampleThirdStartup => Dependencies size: 2 => callCreateOnMainThread: false => waitOnMainThread: false
order [3] Class: SampleFourthStartup => Dependencies size: 3 => callCreateOnMainThread: false => waitOnMainThread: false
================================================ ordering end ================================================

完整的代码实例,你可以通过查看app获取。

更多

可选配置

  • LoggerLevel: 控制Android Startup中的日志输出,可选值包括LoggerLevel.NONE, LoggerLevel.ERROR and LoggerLevel.DEBUG。
  • AwaitTimeout: 控制Android Startup中主线程的超时等待时间,即阻塞的最长时间。

Manifest中配置

使用这些配置,你需要定义一个类去实现StartupProviderConfig接口,并且实现它的对应方法。

1
2
3
4
5
6
7
8
kotlin复制代码class SampleStartupProviderConfig : StartupProviderConfig {

override fun getConfig(): StartupConfig =
StartupConfig.Builder()
.setLoggerLevel(LoggerLevel.DEBUG)
.setAwaitTimeout(12000L)
.build()
}

与此同时,你还需要在manifest中进行配置StartupProviderConfig。

1
2
3
4
5
6
7
8
9
10
ini复制代码<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${applicationId}.android_startup"
android:exported="false">

<meta-data
android:name="com.rousetime.sample.startup.SampleStartupProviderConfig"
android:value="android.startup.provider.config" />

</provider>

经过上面的配置,StartupProvider会自动解析SampleStartupProviderConfig。

Application中配置

在Application需要借助StartupManager.Builder()进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码override fun onCreate() {
super.onCreate()

val config = StartupConfig.Builder()
.setLoggerLevel(LoggerLevel.DEBUG)
.setAwaitTimeout(12000L)
.build()

StartupManager.Builder()
.setConfig(config)
...
.build(this)
.start()
.await()
}

方法

AndroidStartup

  • createExecutor(): Executor: 如果定义的组件没有运行在主线程,那么可以通过该方法进行控制运行的子线程。
  • onDependenciesCompleted(startup: Startup<*>, result: Any?): 该方法会在每一个依赖执行完毕之后进行回调。

实战测试

AwesomeGithub中使用了Android Startup,优化配置的初始化时间与组件化开发的配置注入时机,使用前与使用后时间对比:

状态 启动页面 消耗时间
使用前 WelcomeActivity 420ms
使用后 WelcomeActivity 333ms

项目

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件。

AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

微信公众号:【Android补给站】或者扫描下方二维码进行关注

本文转载自: 掘金

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

【Oracle】手动安装和卸载Oracle,这是最全的一篇了

发表于 2020-08-10

写在前面

之前写过一篇在CentOS 7/8上安装Oracle的文章,按照我写的文章安装成功了,但是卸载Oracle时出现了问题。今天,我就整理一篇手动安装和卸载Oracle的文章吧。全文为实战型内容,全程干货。

如果文章对你有所帮助,请不要吝惜你的点赞、在看、留言和转发,你的支持是我持续创作的最大动力!

环境准备

1.CentOS7 / CentOS8 64位最小化安装的虚拟机环境(这里的安装步骤,我就直接省略了,大家自行安装虚拟机环境)

2.Oracle 11gR2 64位 Linux版安装包(关注【冰河技术】微信公众号,回复“oracle”关键字即可获取Oracle数据库安装包下载链接)。

linux.x64_11gR2_database_1of2.zip

linux.x64_11gR2_database_2of2.zip

安装过程

1.关闭防火墙

操作用户:root

1
2
bash复制代码systemctl stop firewalld.service
systemctl disable firewalld.service

2.安装依赖包

操作用户为:root。

执行如下命令安装依赖包。

1
2
3
4
5
6
7
bash复制代码yum install -y automake autotools-dev binutils bzip2 elfutils expat \
gawk gcc gcc-multilib g++-multilib lib32ncurses5 lib32z1 \
ksh less lib32z1 libaio1 libaio-dev libc6-dev libc6-dev-i386 \
libc6-i386 libelf-dev libltdl-dev libodbcinstq4-1 libodbcinstq4-1:i386 \
libpth-dev libpthread-stubs0-dev libstdc++5 make openssh-server rlwrap \
rpm sysstat unixodbc unixodbc-dev unzip x11-utils zlibc unzip cifs-utils \
libXext.x86_64 glibc.i686

3.创建oracle用户

操作用户为:root

1
2
3
4
5
6
bash复制代码groupadd -g 502 oinstall
groupadd -g 503 dba
groupadd -g 504 oper
groupadd -g 505 asmadmin
useradd -u 502 -g oinstall -G oinstall,dba,asmadmin,oper -s /bin/bash -m oracle
passwd oracle

上述命令执行完毕后,为oracle用户设置密码,例如,我这里设置的密码为oracle

4.解压Oracle数据库安装包

操作用户:oracle
操作目录:/home/oracle

将Oracle 11gR2安装文件上传(可以使用sftp上传)到该操作目录下面,然后顺序解压安装文件到该目录。

1
2
bash复制代码unzip linux.x64_11gR2_database_1of2.zip
unzip linux.x64_11gR2_database_2of2.zip

5.修改操作系统配置

操作用户:root
操作文件:/etc/security/limits.conf

1
bash复制代码vim /etc/security/limits.conf

在文件的末尾添加如下配置项。

1
2
3
4
5
bash复制代码oracle          soft      nproc   2047
oracle hard nproc 16384
oracle soft nofile 1024
oracle hard nofile 65536
oracle soft stack 10240

6.创建Oracle安装目录

操作用户:oracle

1
bash复制代码mkdir ~/tools/oracle11g

7.修改环境变量

操作用户:oracle
操作目录:/home/oracle

1
bash复制代码vim ~/.bash_profile

在文件末尾添加如下配置项

1
2
3
4
5
6
bash复制代码export ORACLE_BASE=/home/oracle/tools/oracle11g
export ORACLE_HOME=$ORACLE_BASE/product/11.2.0/dbhome_1
export ORACLE_SID=orcl
export ORACLE_UNQNAME=orcl
export NLS_LANG=.AL32UTF8
export PATH=${PATH}:${ORACLE_HOME}/bin/:$ORACLE_HOME/lib64

使得环境变量生效。

1
bash复制代码source ~/.bash_profile

8.修改Oracle配置文件

操作用户:oracle
操作目录:/home/oracle

复制文件模板

1
bash复制代码cp /home/oracle/database/response/db_install.rsp .

注意:复制命令的最后一个 . 不能省略,表示将db_install.rsp文件从/home/oracle/database/response目录拷贝到当前目录。

对db_install.rsp文件进行编辑。

1
bash复制代码vim db_install.rsp

需要修改的配置项如下所示,这里,我将修改后的配置项列举出来。

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
bash复制代码oracle.install.option=INSTALL_DB_AND_CONFIG
ORACLE_HOSTNAME=localhost #实际上可以修改成你自己的主机名或者域名(IP)
UNIX_GROUP_NAME=oinstall
INVENTORY_LOCATION=/home/oracle/tools/oraInventory
SELECTED_LANGUAGES=en,zh_CN
ORACLE_HOME=/home/oracle/tools/oracle11g/product/11.2.0/dbhome_1
ORACLE_BASE=/home/oracle/tools/oracle11g
oracle.install.db.InstallEdition=EE
oracle.install.db.DBA_GROUP=dba
oracle.install.db.OPER_GROUP=oper
oracle.install.db.config.starterdb.type=GENERAL_PURPOSE
oracle.install.db.config.starterdb.globalDBName=orcl
oracle.install.db.config.starterdb.SID=orcl
oracle.install.db.config.starterdb.characterSet=AL32UTF8
oracle.install.db.config.starterdb.memoryOption=true
oracle.install.db.config.starterdb.memoryLimit=1024
oracle.install.db.config.starterdb.installExampleSchemas=false
oracle.install.db.config.starterdb.password.ALL=Oracle#123456
oracle.install.db.config.starterdb.control=DB_CONTROL
oracle.install.db.config.starterdb.dbcontrol.enableEmailNotification=false
oracle.install.db.config.starterdb.dbcontrol.emailAddress=test@qq.com #可以填写你自己的邮箱地址
oracle.install.db.config.starterdb.automatedBackup.enable=false
oracle.install.db.config.starterdb.storageType=FILE_SYSTEM_STORAGE
oracle.install.db.config.starterdb.fileSystemStorage.dataLocation=/home/oracle/tools/oracle11g/oradata
oracle.install.db.config.starterdb.fileSystemStorage.recoveryLocation=/home/oracle/tools/oracle11g/fast_recovery_area
oracle.install.db.config.starterdb.automatedBackup.enable=false
DECLINE_SECURITY_UPDATES=true

9.静默安装Oracle 11gR2

操作用户:oracle
操作目录:/home/oracle/database

1
bash复制代码./runInstaller -silent -ignoreSysPrereqs -responseFile /home/oracle/db_install.rsp

接下来,就是默默的等待Oracle自行安装了,等待一段时间后,如果输出如下信息,则表明Oracle数据库已经安装成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码The following configuration scripts need to be executed as the "root" user.
#!/bin/sh
#Root scripts to run

/home/oracle/tools/oraInventory/orainstRoot.sh
/home/oracle/tools/oracle11g/product/11.2.0/dbhome_1/root.sh
To execute the configuration scripts:

1. Open a terminal window
2. Log in as "root"
3. Run the scripts
4. Return to this window and hit "Enter" key to continue

Successfully Setup Software.

10.安装完成

操作用户:root

根据上一步完成信息提示,执行以下两行命令,具体位置需要根据你的安装位置决定:

1
2
bash复制代码/home/oracle/tools/oraInventory/orainstRoot.sh
/home/oracle/tools/oracle11g/product/11.2.0/dbhome_1/root.sh

11.创建连接用户

操作用户:oracle

1
2
3
bash复制代码sqlplus /nolog
conn /as sysdba
startup

接下来,执行如下命令。

1
2
bash复制代码alter user system identified by system;
alter user sys identified by sys;

创建连接用户。

1
2
bash复制代码create user SYNC identified by SYNC;
grant connect,resource,dba to SYNC;

验证安装结果

1.启动数据库

启动已经安装的数据库orcl。

操作用户oracle

1
bash复制代码sqlplus /nolog

使用dba权限连接Oralce

1
bash复制代码connect / as sysdba

启动数据库

1
bash复制代码startup

确认启动结果:

1
2
3
4
5
6
7
8
9
bash复制代码ORACLE instance started.

Total System Global Area 534462464 bytes
Fixed Size 2215064 bytes
Variable Size 373293928 bytes
Database Buffers 150994944 bytes
Redo Buffers 7958528 bytes
Database mounted.
Database opened.

2.验证数据库

这里,我们使用Navicat连接Oracle数据库,如下所示。

这里,输入的用户名为SYNC,密码为SYNC。

接下来,点击“连接测试”,如下所示。

可以看到,Oracle数据库连接成功。

手动卸载Oracle

1.停止监听

1
2
bash复制代码[oracle@binghe101 ~]$ lsnrctl stop
[oracle@binghe101 ~]$ lsnrctl status

2.停止数据库

1
2
bash复制代码[oracle@binghe101 ~]$ sqlplus / as sysdba
SQL> shutdown immediate

3.删除oracle的inventory 目录

1
bash复制代码[root@binghe101 app]# rm -rf /home/oracle/tools/oraInventory/

4.删除Oracle的base目录下所有的目录

1
bash复制代码[root@binghe101 oracle]# rm -rf /home/oracle/tools/oracle11g/*

5.删除临时目录/tmp

1
bash复制代码[root@binghe101 tmp]# rm -rf /tmp/*

6.删除Oracle的配置文件

1
bash复制代码[root@binghe101 tmp]# rm -f /etc/ora*

7.删除oracle产生命令

1
bash复制代码[root@binghe101 tmp]# rm -f /usr/local/bin/*

8.其他的文件

1
bash复制代码[root@binghe101 .oracle]# rm -rf /usr/tmp/.oracle/

9.删除用户和组

1
2
3
4
5
bash复制代码[root@binghe101 tmp]# userdel -r oracle
[root@binghe101 tmp]# groupdel oper
[root@binghe101 tmp]# groupdel dba
[root@binghe101 tmp]# groupdel oinstall
[root@binghe101 tmp]# groupdel asmadmin

10.撤销oracle的资源限制文件

1
bash复制代码[root@binghe101 tmp]# vi /etc/security/limits.conf

11.内核参数

1
2
bash复制代码[root@binghe101 tmp]# vi /etc/sysctl.conf 
[root@binghe101 tmp]# sysctl -p

12.删除oracle base

1
bash复制代码[root@binghe101 ~]# rm -rf /home/oracle/tools/oracle11g

重磅福利

关注「 冰河技术 」微信公众号,后台回复 “设计模式” 关键字领取《深入浅出Java 23种设计模式》PDF文档。回复“Java8”关键字领取《Java8新特性教程》PDF文档。回复“限流”关键字获取《亿级流量下的分布式限流解决方案》PDF文档,三本PDF均是由冰河原创并整理的超硬核教程,面试必备!!

好了,今天就聊到这儿吧!别忘了点个赞,给个在看和转发,让更多的人看到,一起学习,一起进步!!

写在最后

如果你觉得冰河写的还不错,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发、分布式、微服务、大数据、互联网和云原生技术,「 冰河技术 」微信公众号更新了大量技术专题,每一篇技术文章干货满满!不少读者已经通过阅读「 冰河技术 」微信公众号文章,吊打面试官,成功跳槽到大厂;也有不少读者实现了技术上的飞跃,成为公司的技术骨干!如果你也想像他们一样提升自己的能力,实现技术能力的飞跃,进大厂,升职加薪,那就关注「 冰河技术 」微信公众号吧,每天更新超硬核技术干货,让你对如何提升技术能力不再迷茫!

本文转载自: 掘金

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

看阿里P7讲MyBatis:从MyBatis的理解以及配置和

发表于 2020-08-10

前言

MyBatis 是一款优秀的持久层框架,一个半 ORM(对象关系映射)框架,它支持定制化 SQL、存储过程以及高级映`射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

MyBatis的作用

MyBatis作用是在持久层也就是访问数据库的操作,以前我们访问数据库是用JDBC来访问数据库,JDBC访问数据库需要写很多重复的代码,假如数据库访问很多还需要对数据库连接进行不停的打开连接和关闭连接,很消耗系统性能
MyBatis封装了JDBC底层访问数据库的代码,让我们程序猿只需要关心如何去写好SQL就好,不在需要去写JDBC底层的代码

MyBatis的优缺点

优点

  • MyBatis封装了JBDC底层访问数据库的细节,使我们程序猿不需要与JDBC API打交道,就可以访问数据库
  • MyBatis简单易学,程序猿直接编写SQL语句,适合于对SQL语句性能要求比较高的项目
  • SQL语句封装在配置文件中,便于统一管理与维护,降低了程序的耦合度
  • SQL代码从程序代码中彻底分离出来,可重用
  • 提供了动态SQL标签,支持编写动态SQL
  • 提供映射标签,支持对象与数据库的ORM字段关系映射

缺点

  • 过于依赖数据库SQL语句,导致数据库移植性差,更换数据库,如果SQL语句有差异,SQL语句工作量大
  • 由于xml里标签id必须唯一,导致DAO中方法不支持方法重载

MyBatis的配置文件

properties元素

properties元素描述的都是外部化,可替代的属性
一般用来配置连接数据源,我们可以使用property子节点来配置也可以使用资源路径引用

使用property子节点来配置

1
2
3
4
5
6
ini复制代码    <properties>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</properties>

使用资源路径引用

1
ini复制代码    <properties resource="jdbcConfig.properties"/>

jdbcConfig.properties里面的属性

1
2
3
4
ini复制代码driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test
username=root
password=123456

连接数据源的配置

1
2
3
4
5
6
ini复制代码	<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>

假如使用property子节点来配置和使用资源路径引用都用了,这个时候MyBatis会调用哪个勒?MyBatis会调用资源路径引用的属性值,因为资源路径引用的优先级高于property子节点的优先级

settings元素

settings是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为

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
xml复制代码    <settings>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true"/>

<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="true"/>

<!-- 设置关联对象加载的形态,此处为按需加载字段 (加载字段由 SQL指 定 ),不会加载关联表的所有字段,以提高性能 -->
<setting name="aggressiveLazyLoading" value="false"/>

<!-- 对于未知的 SQL查询,允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true"/>

<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true"/>

<!-- 允许使用自定义的主键值 (比如由程序生成的 UUID 32位编码作为键值 ),数据表的 PK生成策略将被覆盖 -->
<setting name="useGeneratedKeys" value="true"/>

<!-- 给予被嵌套的 resultMap以字段 -属性的映射支持 -->
<setting name="autoMappingBehavior" value="FULL"/>

<!-- 对于批量更新操作缓存 SQL以提高性能 -->
<setting name="defaultExecutorType" value="BATCH"/>

<!-- 数据库超过 25000秒仍未响应则超时 -->
<setting name="defaultStatementTimeout" value="25000"/>
</settings>

typeAliases元素

typeAliases元素的作用是给JavaBean取别名,方便我们在mappeer配置文件中使用

当我们没有给JavaBean取别名,mapper配置文件中获取JavaBean的时候,我们就需要获取JavaBean所在项目里面的全路径

1
2
3
4
xml复制代码    <!--省略部分代码-->
<select id="login" resultType="cn.friday.pojo.DevUser">
SELECT * FROM dev_user WHERE devCode=#{devCode} AND devPassword=#{devPassword}
</select>

接下来我们就来给JavaBean取别名

1
2
3
4
bash复制代码    <typeAliases>
<typeAlias type="cn.friday.pojo.DevUser" alias="devUser"/>
<typeAlias type="cn.friday.pojo.AppInfo" alias="appInfo"/>
</typeAliases>

给每个JavaBean去取一个指定的别名,这样是有缺陷的,万一项目中有很多个POJO那么工作量就大了,不过还有一种方法给指定的包里面所有的JavaBean都取一个别名,MyBatis会自动扫描所指定的包下的JavaBean并且给一个默认的别名,默认的别名为JavaBean的名称,请看下面

1
2
3
xml复制代码    <typeAliases>
<package name="cn.friday.pojo"/>
</typeAliases>

mapper里面的配置文件就可以正常使用JavaBean取的别名了,不需要再去获取JavaBean的全路径了

1
2
3
4
xml复制代码    <!--省略部分代码-->
<select id="login" resultType="DevUser">
SELECT * FROM dev_user WHERE devCode=#{devCode} AND devPassword=#{devPassword}
</select>

environments元素

MyBatis可以配置多种环境,如开发环境、测试环境、生产环境等,我们可以灵活选择不同的配置,从而将SQL映射应用到不同的数据库环境上.这些不同的运行环境我们就可以用environments元素来配置实现

environments元素元素的配置

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
xml复制代码	<!--开发环境-->
<!--default属性表示在默认的情况下我们将启用的数据源-->
<environments default="development">
<!--id属性用来标识一个数据源的,方便在MyBatis中使用 -->
<environment id="development">
<!-- 使用jdbc事务管理 -->
<transactionManager type="JDBC"/>
<!-- 配置数据库连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>

<!--我们在来配置一个测试环境-->
<environment id="test">
<!-- 使用jdbc事务管理 -->
<transactionManager type="JDBC"/>
<!-- 配置数据库连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test1"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

假如我们想从开发环境变成测试环境只需要修改environments元素里面的default属性即可

1
2
3
xml复制代码 <environments default="test">
<!--省略部分代码-->
</environments>

mappers元素

mappers映射器,说简单点就是告诉MyBatis去哪里找到SQL语句映射文件,我们可以使用类资源路径或者是URL等
用类资源路径获取映射文件

1
2
3
4
ini复制代码    <mappers>
<mapper resource="cn/friday/dao/developer/DevUserMapper.xml"/>
<mapper resource="cn/friday/dao/developer/AppInfoMapper.xml"/>
</mappers>

用URL获取映射文件

1
2
3
4
ini复制代码    <mappers>
<mapper url="file:///D:/mappers/DevUserMapper.xml"/>
<mapper url="file:///D:/mappers/AppInfoMapper.xml"/>
</mappers>

如何实现MyBatis

先给大家看一下我的项目结构

第一步 导入依赖

我的是maven项目所以只需要在pox.xml配置文件中添加关于MyBatis的依赖即可

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>MyBatis</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.4</version>
</dependency>

</dependencies>

<build>
<resources>
<resource>
<directory>src/main/java/</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>

第二步 创建MyBatis配置文件

这些配置文件上面也用讲的过这里就不做过多的解释了

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc-config.properties"/>

<typeAliases>
<package name="com.friday.pojo"/>
</typeAliases>

<environments default="test">
<environment id="test">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="com/friday/dao/UserMapper.xml"></mapper>
</mappers>
</configuration>

jdbc-config.properties里面的属性

1
2
3
4
ini复制代码driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC
username=root
password=123456

第三步 创建接口以及映射文件

接口,普通的java接口

1
2
3
4
5
6
7
8
9
less复制代码package com.friday.dao;

import com.friday.pojo.User;
import org.apache.ibatis.annotations.Param;

public interface UserMapper {
//@Param相对应给String userCode取了一个别名叫做userPassword,我们到写映射SQL语句的时候只有#{注解名称}即可,如#{userPassword}
public User login(@Param("userCode") String userCode,@Param("userPassword") String pwd);
}

映射文件

1
2
3
4
5
6
7
8
9
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.friday.dao.UserMapper">
<select id="login" resultType="User" parameterType="string">
SELECT * FROM smbms_user WHERE userCode=#{userCode} AND userPassword=#{userPassword}
</select>
</mapper>

mapper 文件里面的属性

  • namespace属性 指定相对应的接口
  • id属性 接口里面具体的方法名
  • resultType 返回值的类型
  • resultType 传进来的参数的类型

第四步 测试

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
java复制代码package com.friday.test;

import com.friday.dao.UserMapper;
import com.friday.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class MyBatisTest {
public static void main(String[] args) throws IOException {
//读取mybatis配置文件
String resource = "mybatis-config.xml";
//获取mybatis配置文件的输入流
InputStream is = Resources.getResourceAsStream(resource);
//创建SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
//打开sqlSession对象
SqlSession sqlSession = factory.openSession();

//获取对应的Mapper,让映射器通过命名空间和方法名称找到对应的SQL,发送给数据库执行后返回结果。
User user = sqlSession.getMapper(UserMapper.class).login("zhanghua","userPassword");

//看一下是否可以查到数据
if (user != null) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}

//关闭sqlSession对象
sqlSession.close();
}
}

最后

大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料放在里面,助你圆梦BAT!文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!

本文转载自: 掘金

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

超详细Canal入门,看这篇就够了! 思维导图 前言 一、什

发表于 2020-08-10

思维导图

在这里插入图片描述

本文章已收录到个人博客网站(我爱B站):me.lovebilibili.com

前言

我们都知道一个系统最重要的是数据,数据是保存在数据库里。但是很多时候不单止要保存在数据库中,还要同步保存到Elastic Search、HBase、Redis等等。

这时我注意到阿里开源的框架Canal,他可以很方便地同步数据库的增量数据到其他的存储应用。所以在这里总结一下,分享给各位读者参考~

一、什么是canal

我们先看官网的介绍

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

这句介绍有几个关键字:增量日志,增量数据订阅和消费。

这里我们可以简单地把canal理解为一个用来同步增量数据的一个工具。

接下来我们看一张官网提供的示意图:

canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

二、canal能做什么

以下参考canal官网。

与其问canal能做什么,不如说数据同步有什么作用。

但是canal的数据同步不是全量的,而是增量。基于binary log增量订阅和消费,canal可以做:

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护
  • 业务cache(缓存)刷新
  • 带业务逻辑的增量数据处理

三、如何搭建canal

3.1 首先有一个MySQL服务器

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

我的Linux服务器安装的MySQL服务器是5.7版本。

MySQL的安装这里就不演示了,比较简单,网上也有很多教程。

然后在MySQL中需要创建一个用户,并授权:

1
2
3
4
5
sql复制代码-- 使用命令登录:mysql -u root -p
-- 创建用户 用户名:canal 密码:Canal@123456
create user 'canal'@'%' identified by 'Canal@123456';
-- 授权 *.*表示所有库
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal@123456';

下一步在MySQL配置文件my.cnf设置如下信息:

1
2
3
4
5
6
7
yml复制代码[mysqld]
# 打开binlog
log-bin=mysql-bin
# 选择ROW(行)模式
binlog-format=ROW
# 配置MySQL replaction需要定义,不要和canal的slaveId重复
server_id=1

改了配置文件之后,重启MySQL,使用命令查看是否打开binlog模式:

在这里插入图片描述

查看binlog日志文件列表:

在这里插入图片描述

查看当前正在写入的binlog文件:

在这里插入图片描述

MySQL服务器这边就搞定了,很简单。

3.2 安装canal

去官网下载页面进行下载:github.com/alibaba/can…

我这里下载的是1.1.4的版本:

在这里插入图片描述

解压canal.deployer-1.1.4.tar.gz,我们可以看到里面有四个文件夹:

接着打开配置文件conf/example/instance.properties,配置信息如下:

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
properties复制代码## mysql serverId , v1.0.26+ will autoGen
## v1.0.26版本后会自动生成slaveId,所以可以不用配置
# canal.instance.mysql.slaveId=0

# 数据库地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名称
canal.instance.master.journal.name=mysql-bin.000001
# mysql主库链接时起始的binlog偏移量
canal.instance.master.position=154
# mysql主库链接时起始的binlog的时间戳
canal.instance.master.timestamp=
canal.instance.master.gtid=

# username/password
# 在MySQL服务器授权的账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@123456
# 字符集
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false

# table regex .*\\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\\..*
# mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=

我这里用的是win10系统,所以在bin目录下找到startup.bat启动:

启动就报错,坑呀:

要修改一下启动的脚本startup.bat:

在这里插入图片描述

然后再启动脚本:

在这里插入图片描述

这就启动成功了。

Java客户端操作

首先引入maven依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>

然后创建一个canal项目,使用SpringBoot构建,如图所示:

在这里插入图片描述

在CannalClient类使用Spring Bean的生命周期函数afterPropertiesSet():

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
java复制代码@Component
public class CannalClient implements InitializingBean {

private final static int BATCH_SIZE = 1000;

@Override
public void afterPropertiesSet() throws Exception {
// 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1", 11111), "example", "", "");
try {
//打开连接
connector.connect();
//订阅数据库表,全部表
connector.subscribe(".*\\..*");
//回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿
connector.rollback();
while (true) {
// 获取指定数量的数据
Message message = connector.getWithoutAck(BATCH_SIZE);
//获取批量ID
long batchId = message.getId();
//获取批量的数量
int size = message.getEntries().size();
//如果没有数据
if (batchId == -1 || size == 0) {
try {
//线程休眠2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//如果有数据,处理数据
printEntry(message.getEntries());
}
//进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。
connector.ack(batchId);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connector.disconnect();
}
}

/**
* 打印canal server解析binlog获得的实体类信息
*/
private static void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
//开启/关闭事务的实体类型,跳过
continue;
}
//RowChange对象,包含了一行数据变化的所有特征
//比如isDdl 是否是ddl变更操作 sql 具体的ddl sql beforeColumns afterColumns 变更前后的数据字段等等
RowChange rowChage;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e);
}
//获取操作类型:insert/update/delete类型
EventType eventType = rowChage.getEventType();
//打印Header信息
System.out.println(String.format("================》; binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
eventType));
//判断是否是DDL语句
if (rowChage.getIsDdl()) {
System.out.println("================》;isDdl: true,sql:" + rowChage.getSql());
}
//获取RowChange对象里的每一行数据,打印出来
for (RowData rowData : rowChage.getRowDatasList()) {
//如果是删除语句
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
//如果是新增语句
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
//如果是更新的语句
} else {
//变更前的数据
System.out.println("------->; before");
printColumn(rowData.getBeforeColumnsList());
//变更后的数据
System.out.println("------->; after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}

private static void printColumn(List<Column> columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}

以上就完成了Java客户端的代码。这里不做具体的处理,仅仅是打印,先有个直观的感受。

最后我们开始测试,首先启动MySQL、Canal Server,还有刚刚写的Spring Boot项目。然后创建表:

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `tb_commodity_info` (
`id` varchar(32) NOT NULL,
`commodity_name` varchar(512) DEFAULT NULL COMMENT '商品名称',
`commodity_price` varchar(36) DEFAULT '0' COMMENT '商品价格',
`number` int(10) DEFAULT '0' COMMENT '商品数量',
`description` varchar(2048) DEFAULT '' COMMENT '商品描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';

然后我们在控制台就可以看到如下信息:

如果新增一条数据到表中:

1
sql复制代码INSERT INTO tb_commodity_info VALUES('3e71a81fd80711eaaed600163e046cc3','叉烧包','3.99',3,'又大又香的叉烧包,老人小孩都喜欢');

控制台可以看到如下信息:

在这里插入图片描述

总结

canal的好处在于对业务代码没有侵入,因为是基于监听binlog日志去进行同步数据的。实时性也能做到准实时,其实是很多企业一种比较常见的数据同步的方案。

通过上面的学习之后,我们应该都明白canal是什么,它的原理,还有用法。实际上这仅仅只是入门,因为实际项目中我们不是这样玩的…

实际项目我们是配置MQ模式,配合RocketMQ或者Kafka,canal会把数据发送到MQ的topic中,然后通过消息队列的消费者进行处理。

Canal的部署也是支持集群的,需要配合ZooKeeper进行集群管理。

Canal还有一个简单的Web管理界面。

下一篇就讲一下集群部署Canal,配合使用Kafka,同步数据到Redis。

参考资料:Canal官网

絮叨

上面所有例子的代码都上传Github了:

github.com/yehongzhi/m…

如果你觉得这篇文章对你有用,点个赞吧~

你的点赞是我创作的最大动力~

想第一时间看到我更新的文章,可以微信搜索公众号「java技术爱好者」,拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文转载自: 掘金

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

Kotlin Jetpack 实战 07 Kotlin

发表于 2020-08-10

往期文章

《Kotlin Jetpack 实战:开篇》

《00. 写给 Java 开发者的 Kotlin 入坑指南》

《03. Kotlin 编程的三重境界》

《04. Kotlin 高阶函数》

《05. Kotlin 泛型》

《06. Kotlin 扩展》

  1. 前言

委托(Delegation),可能是 Kotlin 里最容易被低估的特性。

提到 Kotlin,大家最先想起的可能是扩展,其次是协程,再要不就是空安全,委托根本排不上号。但是,在一些特定场景中,委托的作用是无比犀利的。

本文将系统介绍 Kotlin 的委托,然后在实战环节中,我会尝试用委托 + 扩展函数 + 泛型,来封装一个功能相对完整的 SharedPreferences 框架。

  1. 前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:
    github.com/chaxiu/Kotl…
  • 切换到分支:chapter_07_delegate
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓
  1. 委托类(Class Delegation)

委托类,通过关键字 by 可以很方便的实现语法级别的委托模式。看个简单例子:

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
kotlin复制代码interface DB {
fun save()
}

class SqlDB() : DB {
override fun save() { println("save to sql") }
}

class GreenDaoDB() : DB {
override fun save() { println("save to GreenDao") }
}
// 参数 通过 by 将接口实现委托给 db
// ↓ ↓
class UniversalDB(db: DB) : DB by db

fun main() {
UniversalDB(SqlDB()).save()
UniversalDB(GreenDaoDB()).save()
}

/*
输出:
save to sql
save to GreenDao
*/

这种委托模式在我们实际编程中十分常见,UniversalDB 相当于一个壳,它提供数据库存储功能,但并不关心它怎么实现。具体是用 Sql 还是 GreenDao,传不同的委托对象进去就行。

以上委托类的写法,等价于以下 Java 代码:

1
2
3
4
5
6
7
java复制代码class UniversalDB implements DB {
DB db;
public UniversalDB(DB db) { this.db = db; }
// 手动重写接口,将 save 委托给 db.save()
@Override// ↓
public void save() { db.save(); }
}

各位可不要小看这个小小的by,上面的例子中,接口只有一个方法,所以 Java 看起来也不怎么麻烦,但是,当我们想委托的接口方法很多的时候,这个by能极大的减少我们的代码量。

我们看一个复杂点的例子,假设我们想对MutableList进行封装,并且增加一个方法,借助委托类的 by,几行代码就搞定了:

1
2
3
4
5
6
7
8
kotlin复制代码//                               这个参数才是干活的,所有接口实现都被委托给它了,实现这一切只需要  ↓
// ↓
class LogList(val log: () -> Unit, val list: MutableList<String>) : MutableList<String> by list{
fun getAndLog(index: Int): String {
log()
return get(index)
}
}

如果是在 Java 里,那就不好意思了,呵呵,我们必须 implements 这么多方法:

想想要写那么多的重复代码就心累,是不是?

注:Effective Java 里面提到过:组合优于继承(Favor composition over inheritance),所以在 Java 中,我们也会尽可能多使用接口(interface)。借助 Kotlin 提供的委托类,我们使用组合类会更方便。结合上面的例子,如果需要实现的接口有很多个,委托类真的可以帮我们省下许多的代码量。

  1. 委托属性(Property Delegation)

委托属性,它和委托类虽然都是通过 by 来使用的,但是它们完全不是一回事。委托类委托出去的是它的接口实现;委托属性,委托出去的是属性的 getter,setter。我们前面经常提到的 val text = by lazy{},其实就是将 text 的 getter 委托给了 lazy{}。

1
2
3
4
5
6
arduino复制代码val text: String = by lazy{}
// 它的原理其实跟下面是一样的

// 语法层面上是等价的哈,实际我们不能这么写
val text: String
get() { lazy{} }
  1. 自定义委托属性

Kotlin 的委托属性用起来很神奇,那我们怎么根据需求实现自己的属性委托呢?看看下面的例子:

1
2
3
kotlin复制代码class Owner {
var text: String = “Hello”
}

我想为上面的 text 属性提供委托,应该怎么做?请看下面例子的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class StringDelegate(private var s: String = "Hello") {
// 对应 text 所处的类 对应 text 的类型
// ⚡ 👇 ↓
operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
return s
}
// 对应 text 所处的类 对应 text 的类型
// ⚡ 👇 ↓
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
s = value
}
}

// 👇
class Owner {
↓
var text: String by StringDelegate()
}

小结:

  • var —— 我们需要提供getValue和setValue
  • val —— 则只需要 getValue
  • operator —— 是必须的,这是编译器识别委托属性的关键。注释中已用 ⚡ 标注了。
  • property —— 它的类型一般固定写成 KProperty<*>
  • value —— 的类型必须是委托属性的类型,或者是它的父类。也就是说例子中的 value: String 也可以换成 value: Any。注释中已用↓标注了。
  • thisRef —— 它的类型,必须是属性所有者的类型,或者是它的父类。也就是说例子中的thisRef: Owner 也可以换成 thisRef: Any。注释中已用 👇 标注了。

以上是委托属性中比较重要的细节,把握好这些细节,我们写自定义委托就没什么问题了。

  1. 实战

又到了我们熟悉的实战环节,让我们来做点有意思的事情吧。

  1. 热身

前面的章节我们实现过一个简单的 HTML DSL,一起看看如何使用委托来优化之前的代码吧!如果仔细看的话,各位应该能发现这一处代码看着非常不爽,明显的模版代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码class IMG : BaseElement("img") {
var src: String
get() = hashMap["src"]!!
set(value) {
hashMap["src"] = value
}
// ↑
// 看看这重复的模板代码
// ↓
var alt: String
get() = hashMap["alt"]!!
set(value) {
hashMap["alt"] = value
}
}

要是能用委托属性来写就好了:

1
2
3
4
5
kotlin复制代码// 这代码看着真舒服
class IMG : BaseElement("img") {
var src: String by hashMap
var alt: String by hashMap
}

按照前面讲的自定义委托属性的要求,我们很容易就能写出这样的代码:

1
2
3
4
5
6
7
kotlin复制代码//                                                  对应 IMG 类
// 👇
operator fun HashMap<String, String?>.getValue(thisRef: IMG, property: KProperty<*>): String? =
get(property.name)

operator fun HashMap<String, String>.setValue(thisRef: IMG, property: KProperty<*>, value: String) =
put(property.name, value)

按照前面讲的,thisRef 的类型可以是父类,所以写成这样问题也不大:

1
2
3
4
5
6
7
kotlin复制代码//                                                  变化在这里
// 👇
operator fun HashMap<String, String?>.getValue(thisRef: Any, property: KProperty<*>): String? =
get(property.name)

operator fun HashMap<String, String>.setValue(thisRef: Any, property: KProperty<*>, value: String) =
put(property.name, value)

改成 thisRef: Any 的好处是,以后在任意类里面的 String 属性,我们都可以用这种方式去委托了,比如:

1
2
3
4
kotlin复制代码class Test {
var src: String by hashMap
var alt: String by hashMap
}

思考题1

请问:上面的 thisRef: Any 改成 thisRef: Any? 是否会更好?为什么?

思考题2

官方其实有 map 委托的实现,官方的写法好在哪里?(答案藏在 GitHub Demo 代码注释里。)

  1. 委托属性 + SharedPreferences

在上一章 扩展函数,我们使用高阶函数+扩展函数,简化了 SharedPreferences,但那个用法仍然不够简洁,那时候我们是这么用的,说实话,还不如我们 Java 封装的 PreferenceUtils 呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码// MainActivity.kt
private val preference: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) {
getSharedPreferences(SP_NAME, MODE_PRIVATE)
}

// 读取缓存
private val spResponse: String? by lazy(LazyThreadSafetyMode.NONE) {
preference.getString(SP_KEY_RESPONSE, "")
}

private fun display(response: String?) {
// 更新缓存
preference.edit { putString(SP_KEY_RESPONSE, response) }
}

假如我们能这么做呢:

1
2
3
4
5
6
7
kotlin复制代码private var spResponse: String by PreferenceString(SP_KEY_RESPONSE, "")

// 读取,展示缓存
display(spResponse)

// 更新缓存
spResponse = response

这就很妙了!

这样一个委托属性其实也很容易实现对不对?

1
2
3
4
5
6
7
kotlin复制代码operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return prefs.getString(name, "") ?: default
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
prefs.edit().apply()
}

为了让它支持默认值,commit(),我们加两个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码class PreferenceString(
private val name: String,
private val default:String ="",
private val isCommit: Boolean = false,
private val prefs: SharedPreferences = App.prefs) {

operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return prefs.getString(name, default) ?: default
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
with(prefs.edit()){
putString(name, value)
if (isCommit) {
commit()
} else {
apply()
}
}
}
}

这也很简单,对不对?

以上代码仅支持 String 类型,为了让我们的框架支持不同类型的参数,我们可以引入泛型:

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
kotlin复制代码class PreDelegate<T>(
private val name: String,
private val default: T,
private val isCommit: Boolean = false,
private val prefs: SharedPreferences = App.prefs) {

operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return getPref(name, default) ?: default
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
value?.let {
putPref(name, value)
}
}

private fun <T> getPref(name: String, default: T): T? = with(prefs) {
val result: Any? = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type is not supported")
}

result as? T
}

private fun <T> putPref(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type is not supported")
}

if (isCommit) {
commit()
} else {
apply()
}
}
}

以上所有代码都在 GitHub,欢迎 star fork:github.com/chaxiu/Kotl…。

Delegation 测试代码的细节看这个 GitHub Commit

Delegation HTML 的细节看这个 GitHub Commit

Delegation SharedPreferences 的细节看这个 GitHub Commit

思考题3:

以上代码仅支持了几个基础类型,能否扩展支持更多的类型?

思考题4:

以这样的封装方式,下次我们想为 PreDelegate 增加其他框架支持,比如腾讯的 MMKV,应该怎么做?

思考题5:

有没有更优雅的方式封装 SharedPreferences?

  1. 结尾:

  • 委托,分为委托类,委托属性
  • 委托类,可以方便快捷的实现 委托模式,也可以配合接口来实现类组合
  • 委托属性,既可以提高代码的复用率,还能提高代码的可读性。
  • 委托类,它的原理其实就是编译器将委托者,被委托者两者对应的接口方法绑定。
  • 委托属性,它的原理是因为编译器会识别特定的 getter,setter,如果它们符合特定的签名要求,就会被解析成 Delegation

都看到这了,给点个赞呗!

回目录–>【Kotlin Jetpack 实战】

本文转载自: 掘金

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

使用Go实现GoF的23种设计模式(一)

发表于 2020-08-10

前言

从1995年GoF提出23种设计模式到现在,25年过去了,设计模式依旧是软件领域的热门话题。在当下,如果你不会一点设计模式,都不好意思说自己是一个合格的程序员。设计模式通常被定义为:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。

从定义上看,设计模式其实是一种经验的总结,是针对特定问题的简洁而优雅的解决方案。既然是经验总结,那么学习设计模式最直接的好处就在于可以站在巨人的肩膀上解决软件开发过程中的一些特定问题。然而,学习设计模式的最高境界是习得其中解决问题所用到的思想,当你把它们的本质思想吃透了,也就能做到即使已经忘掉某个设计模式的名称和结构,也能在解决特定问题时信手拈来。

好的东西有人吹捧,当然也会招黑。设计模式被抨击主要因为以下两点:

1、设计模式会增加代码量,把程序逻辑变得复杂。这一点是不可避免的,但是我们并不能仅仅只考虑开发阶段的成本。最简单的程序当然是一个函数从头写到尾,但是这样后期的维护成本会变得非常大;而设计模式虽然增加了一点开发成本,但是能让人们写出可复用、可维护性高的程序。引用《软件设计的哲学》里的概念,前者就是战术编程,后者就是战略编程,我们应该对战术编程Say No!(请移步《一步步降低软件复杂性》)

2、滥用设计模式。这是初学者最容易犯的错误,当学到一个模式时,恨不得在所有的代码都用上,从而在不该使用模式的地方刻意地使用了模式,导致了程序变得异常复杂。其实每个设计模式都有几个关键要素:适用场景、解决方法、优缺点。模式并不是万能药,它只有在特定的问题上才能显现出效果。所以,在使用一个模式前,先问问自己,当前的这个场景适用这个模式吗?

《设计模式》一书的副标题是“可复用面向对象软件的基础”,但并不意味着只有面向对象语言才能使用设计模式。模式只是一种解决特定问题的思想,跟语言无关。就像Go语言一样,它并非是像C++和Java一样的面向对象语言,但是设计模式同样适用。本系列文章将使用Go语言来实现GoF提出的23种设计模式,按照创建型模式(Creational Pattern)、结构型模式(Structural Pattern)和行为型模式(Behavioral Pattern)三种类别进行组织,文本主要介绍其中的创建型模式。

单例模式(Singleton Pattern)

单例模式结构

简述

单例模式算是23中设计模式里最简单的一个了,它主要用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。

在程序设计中,有一些对象通常我们只需要一个共享的实例,比如线程池、全局缓存、对象池等,这种场景下就适合使用单例模式。

但是,并非所有全局唯一的场景都适合使用单例模式。比如,考虑需要统计一个API调用的情况,有两个指标,成功调用次数和失败调用次数。这两个指标都是全局唯一的,所以有人可能会将其建模成两个单例SuccessApiMetric和FailApiMetric。按照这个思路,随着指标数量的增多,你会发现代码里类的定义会越来越多,也越来越臃肿。这也是单例模式最常见的误用场景,更好的方法是将两个指标设计成一个对象ApiMetric下的两个实例ApiMetic success和ApiMetic fail。

如何判断一个对象是否应该被建模成单例?

通常,被建模成单例的对象都有“中心点”的含义,比如线程池就是管理所有线程的中心。所以,在判断一个对象是否适合单例模式时,先思考下,这个对象是一个中心点吗?

Go实现

在对某个对象实现单例模式时,有两个点必须要注意:(1)限制调用者直接实例化该对象;(2)为该对象的单例提供一个全局唯一的访问方法。

对于C++/Java而言,只需把类的构造函数设计成私有的,并提供一个static方法去访问该类点唯一实例即可。但对于Go语言来说,即没有构造函数的概念,也没有static方法,所以需要另寻出路。

我们可以利用Go语言package的访问规则来实现,将单例结构体设计成首字母小写,就能限定其访问范围只在当前package下,模拟了C++/Java中的私有构造函数;再在当前package下实现一个首字母大写的访问函数,就相当于static方法的作用了。

在实际开发中,我们经常会遇到需要频繁创建和销毁的对象。频繁的创建和销毁一则消耗CPU,二则内存的利用率也不高,通常我们都会使用对象池技术来进行优化。考虑我们需要实现一个消息对象池,因为是全局的中心点,管理所有的Message实例,所以将其实现成单例,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package msgpool
...
// 消息池
type messagePool struct {
pool *sync.Pool
}
// 消息池单例
var msgPool = &messagePool{
// 如果消息池里没有消息,则新建一个Count值为0的Message实例
pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
}
// 访问消息池单例的唯一方法
func Instance() *messagePool {
return msgPool
}
// 往消息池里添加消息
func (m *messagePool) AddMsg(msg *Message) {
m.pool.Put(msg)
}
// 从消息池里获取消息
func (m *messagePool) GetMsg() *Message {
return m.pool.Get().(*Message)
}
...

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package test
...
func TestMessagePool(t *testing.T) {
msg0 := msgpool.Instance().GetMsg()
if msg0.Count != 0 {
t.Errorf("expect msg count %d, but actual %d.", 0, msg0.Count)
}
msg0.Count = 1
msgpool.Instance().AddMsg(msg0)
msg1 := msgpool.Instance().GetMsg()
if msg1.Count != 1 {
t.Errorf("expect msg count %d, but actual %d.", 1, msg1.Count)
}
}
// 运行结果
=== RUN TestMessagePool
--- PASS: TestMessagePool (0.00s)
PASS

以上的单例模式就是典型的“饿汉模式”,实例在系统加载的时候就已经完成了初始化。对应地,还有一种“懒汉模式”,只有等到对象被使用的时候,才会去初始化它,从而一定程度上节省了内存。众所周知,“懒汉模式”会带来线程安全问题,可以通过普通加锁,或者更高效的双重检验锁来优化。对于“懒汉模式”,Go语言有一个更优雅的实现方式,那就是利用sync.Once,它有一个Do方法,其入参是一个方法,Go语言会保证仅仅只调用一次该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// 单例模式的“懒汉模式”实现
package msgpool
...
var once = &sync.Once{}
// 消息池单例,在首次调用时初始化
var msgPool *messagePool
// 全局唯一获取消息池pool到方法
func Instance() *messagePool {
// 在匿名函数中实现初始化逻辑,Go语言保证只会调用一次
once.Do(func() {
msgPool = &messagePool{
// 如果消息池里没有消息,则新建一个Count值为0的Message实例
pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
}
})
return msgPool
}
...

建造者模式(Builder Pattern)

建造者模式结构

简述

在程序设计中,我们会经常遇到一些复杂的对象,其中有很多成员属性,甚至嵌套着多个复杂的对象。这种情况下,创建这个复杂对象就会变得很繁琐。对于C++/Java而言,最常见的表现就是构造函数有着长长的参数列表:

1
java复制代码MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)

而对于Go语言来说,最常见的表现就是多层的嵌套实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码obj := &MyObject{
Field1: &Field1 {
Param1: &Param1 {
Val: 0,
},
Param2: &Param2 {
Val: 1,
},
...
},
Field2: &Field2 {
Param3: &Param3 {
Val: 2,
},
...
},
...
}

上述的对象创建方法有两个明显的缺点:(1)对对象使用者不友好,使用者在创建对象时需要知道的细节太多;(2)代码可读性很差。

针对这种对象成员较多,创建对象逻辑较为繁琐的场景,就适合使用建造者模式来进行优化。

建造者模式的作用有如下几个:

1、封装复杂对象的创建过程,使对象使用者不感知复杂的创建逻辑。

2、可以一步步按照顺序对成员进行赋值,或者创建嵌套对象,并最终完成目标对象的创建。

3、对多个对象复用同样的对象创建逻辑。

其中,第1和第2点比较常用,下面对建造者模式的实现也主要是针对这两点进行示例。

Go实现

考虑如下的一个Message结构体,其主要有Header和Body组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package msg
...
type Message struct {
Header *Header
Body *Body
}
type Header struct {
SrcAddr string
SrcPort uint64
DestAddr string
DestPort uint64
Items map[string]string
}
type Body struct {
Items []string
}
...

如果按照直接的对象创建方式,创建逻辑应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// 多层的嵌套实例化
message := msg.Message{
Header: &msg.Header{
SrcAddr: "192.168.0.1",
SrcPort: 1234,
DestAddr: "192.168.0.2",
DestPort: 8080,
Items: make(map[string]string),
},
Body: &msg.Body{
Items: make([]string, 0),
},
}
// 需要知道对象的实现细节
message.Header.Items["contents"] = "application/json"
message.Body.Items = append(message.Body.Items, "record1")
message.Body.Items = append(message.Body.Items, "record2")

虽然Message结构体嵌套的层次不多,但是从其创建的代码来看,确实存在对对象使用者不友好和代码可读性差的缺点。下面我们引入建造者模式对代码进行重构:

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
go复制代码package msg
...
// Message对象的Builder对象
type builder struct {
once *sync.Once
msg *Message
}
// 返回Builder对象
func Builder() *builder {
return &builder{
once: &sync.Once{},
msg: &Message{Header: &Header{}, Body: &Body{}},
}
}
// 以下是对Message成员对构建方法
func (b *builder) WithSrcAddr(srcAddr string) *builder {
b.msg.Header.SrcAddr = srcAddr
return b
}
func (b *builder) WithSrcPort(srcPort uint64) *builder {
b.msg.Header.SrcPort = srcPort
return b
}
func (b *builder) WithDestAddr(destAddr string) *builder {
b.msg.Header.DestAddr = destAddr
return b
}
func (b *builder) WithDestPort(destPort uint64) *builder {
b.msg.Header.DestPort = destPort
return b
}
func (b *builder) WithHeaderItem(key, value string) *builder {
// 保证map只初始化一次
b.once.Do(func() {
b.msg.Header.Items = make(map[string]string)
})
b.msg.Header.Items[key] = value
return b
}
func (b *builder) WithBodyItem(record string) *builder {
b.msg.Body.Items = append(b.msg.Body.Items, record)
return b
}
// 创建Message对象,在最后一步调用
func (b *builder) Build() *Message {
return b.msg
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package test
...
func TestMessageBuilder(t *testing.T) {
// 使用消息建造者进行对象创建
message := msg.Builder().
WithSrcAddr("192.168.0.1").
WithSrcPort(1234).
WithDestAddr("192.168.0.2").
WithDestPort(8080).
WithHeaderItem("contents", "application/json").
WithBodyItem("record1").
WithBodyItem("record2").
Build()
if message.Header.SrcAddr != "192.168.0.1" {
t.Errorf("expect src address 192.168.0.1, but actual %s.", message.Header.SrcAddr)
}
if message.Body.Items[0] != "record1" {
t.Errorf("expect body item0 record1, but actual %s.", message.Body.Items[0])
}
}
// 运行结果
=== RUN TestMessageBuilder
--- PASS: TestMessageBuilder (0.00s)
PASS

从测试代码可知,使用建造者模式来进行对象创建,使用者不再需要知道对象具体的实现细节,代码可读性也更好。

工厂方法模式(Factory Method Pattern)

工厂方法模式结构

简述

工厂方法模式跟上一节讨论的建造者模式类似,都是将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口。两者在应用场景上稍有区别,建造者模式更常用于需要传递多个参数来进行实例化的场景。

使用工厂方法来创建对象主要有两个好处:

1、代码可读性更好。相比于使用C++/Java中的构造函数,或者Go中的{}来创建对象,工厂方法因为可以通过函数名来表达代码含义,从而具备更好的可读性。比如,使用工厂方法productA := CreateProductA()创建一个ProductA对象,比直接使用productA := ProductA{}的可读性要好。

2、与使用者代码解耦。很多情况下,对象的创建往往是一个容易变化的点,通过工厂方法来封装对象的创建过程,可以在创建逻辑变更时,避免霰弹式修改。

工厂方法模式也有两种实现方式:(1)提供一个工厂对象,通过调用工厂对象的工厂方法来创建产品对象;(2)将工厂方法集成到产品对象中(C++/Java中对象的static方法,Go中同一package下的函数)

Go实现

考虑有一个事件对象Event,分别有两种有效的时间类型Start和End:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package event
...
type Type uint8
// 事件类型定义
const (
Start Type = iota
End
)
// 事件抽象接口
type Event interface {
EventType() Type
Content() string
}
// 开始事件,实现了Event接口
type StartEvent struct{
content string
}
...
// 结束事件,实现了Event接口
type EndEvent struct{
content string
}
...

1、按照第一种实现方式,为Event提供一个工厂对象,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package event
...
// 事件工厂对象
type Factory struct{}
// 更具事件类型创建具体事件
func (e *Factory) Create(etype Type) Event {
switch etype {
case Start:
return &StartEvent{
content: "this is start event",
}
case End:
return &EndEvent{
content: "this is end event",
}
default:
return nil
}
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package test
...
func TestEventFactory(t *testing.T) {
factory := event.Factory{}
e := factory.Create(event.Start)
if e.EventType() != event.Start {
t.Errorf("expect event.Start, but actual %v.", e.EventType())
}
e = factory.Create(event.End)
if e.EventType() != event.End {
t.Errorf("expect event.End, but actual %v.", e.EventType())
}
}
// 运行结果
=== RUN TestEventFactory
--- PASS: TestEventFactory (0.00s)
PASS

2、按照第二种实现方式,分别给Start和End类型的Event单独提供一个工厂方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package event
...
// Start类型Event的工厂方法
func OfStart() Event {
return &StartEvent{
content: "this is start event",
}
}
// End类型Event的工厂方法
func OfEnd() Event {
return &EndEvent{
content: "this is end event",
}
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package event
...
func TestEvent(t *testing.T) {
e := event.OfStart()
if e.EventType() != event.Start {
t.Errorf("expect event.Start, but actual %v.", e.EventType())
}
e = event.OfEnd()
if e.EventType() != event.End {
t.Errorf("expect event.End, but actual %v.", e.EventType())
}
}
// 运行结果
=== RUN TestEvent
--- PASS: TestEvent (0.00s)
PASS

抽象工厂模式(Abstract Factory Pattern)

抽象工厂模式结构

简述

在工厂方法模式中,我们通过一个工厂对象来创建一个产品族,具体创建哪个产品,则通过swtich-case的方式去判断。这也意味着该产品组上,每新增一类产品对象,都必须修改原来工厂对象的代码;而且随着产品的不断增多,工厂对象的职责也越来越重,违反了单一职责原则。

抽象工厂模式通过给工厂类新增一个抽象层解决了该问题,如上图所示,FactoryA和FactoryB都实现·抽象工厂接口,分别用于创建ProductA和ProductB。如果后续新增了ProductC,只需新增一个FactoryC即可,无需修改原有的代码;因为每个工厂只负责创建一个产品,因此也遵循了单一职责原则。

Go实现

考虑需要如下一个插件架构风格的消息处理系统,pipeline是消息处理的管道,其中包含了input、filter和output三个插件。我们需要实现根据配置来创建pipeline ,加载插件过程的实现非常适合使用工厂模式,其中input、filter和output三类插件的创建使用抽象工厂模式,而pipeline的创建则使用工厂方法模式。

抽象工厂模式示例

各类插件和pipeline的接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package plugin
...
// 插件抽象接口定义
type Plugin interface {}
// 输入插件,用于接收消息
type Input interface {
Plugin
Receive() string
}
// 过滤插件,用于处理消息
type Filter interface {
Plugin
Process(msg string) string
}
// 输出插件,用于发送消息
type Output interface {
Plugin
Send(msg string)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package pipeline
...
// 消息管道的定义
type Pipeline struct {
input plugin.Input
filter plugin.Filter
output plugin.Output
}
// 一个消息的处理流程为 input -> filter -> output
func (p *Pipeline) Exec() {
msg := p.input.Receive()
msg = p.filter.Process(msg)
p.output.Send(msg)
}

接着,我们定义input、filter、output三类插件接口的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package plugin
...
// input插件名称与类型的映射关系,主要用于通过反射创建input对象
var inputNames = make(map[string]reflect.Type)
// Hello input插件,接收“Hello World”消息
type HelloInput struct {}

func (h *HelloInput) Receive() string {
return "Hello World"
}
// 初始化input插件映射关系表
func init() {
inputNames["hello"] = reflect.TypeOf(HelloInput{})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package plugin
...
// filter插件名称与类型的映射关系,主要用于通过反射创建filter对象
var filterNames = make(map[string]reflect.Type)
// Upper filter插件,将消息全部字母转成大写
type UpperFilter struct {}

func (u *UpperFilter) Process(msg string) string {
return strings.ToUpper(msg)
}
// 初始化filter插件映射关系表
func init() {
filterNames["upper"] = reflect.TypeOf(UpperFilter{})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package plugin
...
// output插件名称与类型的映射关系,主要用于通过反射创建output对象
var outputNames = make(map[string]reflect.Type)
// Console output插件,将消息输出到控制台上
type ConsoleOutput struct {}

func (c *ConsoleOutput) Send(msg string) {
fmt.Println(msg)
}
// 初始化output插件映射关系表
func init() {
outputNames["console"] = reflect.TypeOf(ConsoleOutput{})
}

然后,我们定义插件抽象工厂接口,以及对应插件的工厂实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package plugin
...
// 插件抽象工厂接口
type Factory interface {
Create(conf Config) Plugin
}
// input插件工厂对象,实现Factory接口
type InputFactory struct{}
// 读取配置,通过反射机制进行对象实例化
func (i *InputFactory) Create(conf Config) Plugin {
t, _ := inputNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
// filter和output插件工厂实现类似
type FilterFactory struct{}
func (f *FilterFactory) Create(conf Config) Plugin {
t, _ := filterNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
type OutputFactory struct{}
func (o *OutputFactory) Create(conf Config) Plugin {
t, _ := outputNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}

最后定义pipeline的工厂方法,调用plugin.Factory抽象工厂完成pipelien对象的实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package pipeline
...
// 保存用于创建Plugin的工厂实例,其中map的key为插件类型,value为抽象工厂接口
var pluginFactories = make(map[plugin.Type]plugin.Factory)
// 根据plugin.Type返回对应Plugin类型的工厂实例
func factoryOf(t plugin.Type) plugin.Factory {
factory, _ := pluginFactories[t]
return factory
}
// pipeline工厂方法,根据配置创建一个Pipeline实例
func Of(conf Config) *Pipeline {
p := &Pipeline{}
p.input = factoryOf(plugin.InputType).Create(conf.Input).(plugin.Input)
p.filter = factoryOf(plugin.FilterType).Create(conf.Filter).(plugin.Filter)
p.output = factoryOf(plugin.OutputType).Create(conf.Output).(plugin.Output)
return p
}
// 初始化插件工厂对象
func init() {
pluginFactories[plugin.InputType] = &plugin.InputFactory{}
pluginFactories[plugin.FilterType] = &plugin.FilterFactory{}
pluginFactories[plugin.OutputType] = &plugin.OutputFactory{}
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package test
...
func TestPipeline(t *testing.T) {
// 其中pipeline.DefaultConfig()的配置内容见【抽象工厂模式示例图】
// 消息处理流程为 HelloInput -> UpperFilter -> ConsoleOutput
p := pipeline.Of(pipeline.DefaultConfig())
p.Exec()
}
// 运行结果
=== RUN TestPipeline
HELLO WORLD
--- PASS: TestPipeline (0.00s)
PASS

原型模式(Prototype Pattern)

原型模式结构

简述

原型模式主要解决对象复制的问题,它的核心就是clone()方法,返回Prototype对象的复制品。在程序设计过程中,往往会遇到有一些场景需要大量相同的对象,如果不使用原型模式,那么我们可能会这样进行对象的创建:新创建一个相同对象的实例,然后遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。这种方法的缺点很明显,那就是使用者必须知道对象的实现细节,导致代码之间的耦合。另外,对象很有可能存在除了对象本身以外不可见的变量,这种情况下该方法就行不通了。

对于这种情况,更好的方法就是使用原型模式,将复制逻辑委托给对象本身,这样,上述两个问题也都迎刃而解了。

Go实现

还是以建造者模式一节中的Message作为例子,现在设计一个Prototype抽象接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package prototype
...
// 原型复制抽象接口
type Prototype interface {
clone() Prototype
}

type Message struct {
Header *Header
Body *Body
}

func (m *Message) clone() Prototype {
msg := *m
return &msg
}

测试代码如下:

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
go复制代码package test
...
func TestPrototype(t *testing.T) {
message := msg.Builder().
WithSrcAddr("192.168.0.1").
WithSrcPort(1234).
WithDestAddr("192.168.0.2").
WithDestPort(8080).
WithHeaderItem("contents", "application/json").
WithBodyItem("record1").
WithBodyItem("record2").
Build()
// 复制一份消息
newMessage := message.Clone().(*msg.Message)
if newMessage.Header.SrcAddr != message.Header.SrcAddr {
t.Errorf("Clone Message failed.")
}
if newMessage.Body.Items[0] != message.Body.Items[0] {
t.Errorf("Clone Message failed.")
}
}
// 运行结果
=== RUN TestPrototype
--- PASS: TestPrototype (0.00s)
PASS

总结

本文主要介绍了GoF的23种设计模式中的5种创建型模式,创建型模式的目的都是提供一个简单的接口,让对象的创建过程与使用者解耦。其中,单例模式主要用于保证一个类仅有一个实例,并提供一个访问它的全局访问点;建造者模式主要解决需要创建对象时需要传入多个参数,或者对初始化顺序有要求的场景;工厂方法模式通过提供一个工厂对象或者工厂方法,为使用者隐藏了对象创建的细节;抽象工厂模式是对工厂方法模式的优化,通过为工厂对象新增一个抽象层,让工厂对象遵循单一职责原则,也避免了霰弹式修改;原型模式则让对象复制更加简单。

下一篇文章,将介绍23种设计模式中的7种结构型模式(Structural Pattern),及其Go语言的实现。

本文转载自: 掘金

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

图解分析ThreadLocal的原理与应用场景 Thread

发表于 2020-08-08

ThreadLocal的介绍

ThreadLocal这个类想必大家都不陌生,直接翻译为线程本地(变量),我们经常会使用到它来保存一些线程隔离的、全局的变量信息。使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。

ThreadLocal比较像是DNF中的一个地下城副本,而每个线程像是每个进入DNF副本中的玩家。各个线程进入副本后都是比较隔离的,不会互相干扰,这一特性在多线程的某些场景下十分适用。
龙龙的奇妙比喻--ThreadLocal

ThreadLocal介于全局变量与局部变量之间的生命周期

ThreadLocal将变量的使用范围恰当的保存到了全局变量和局部变量之间。

  • 全局变量

静态变量static Object val = new Object() 或 对象的成员变量Object val = new Object(),前者保存在JVM的方法区,后者保存在堆区且随着对象的GC而回收,所有线程都可以访问

  • 局部临时变量

在某个方法中或代码块中声明创建的对象,也保存在堆区,随着代码块的结束因没有引用而被回收,线程独享,但生命周期仅存在于该方法块中

  • ThreadLocal变量

private static ThreadLocal<Object> store = new ThreadLocal<>();,线程独享,且线程执行的任何阶段都可以得到。

ThreadLocal常见的使用场景

笔者经常使用ThreadLocal的场景有:

  • 微服务请求的requestId(或traceId),在微服务模块中用于唯一标记一次请求的全局唯一UUID,在不同模块中传递相同的traceid是通过RPC框架封装并且序列化/反序列化实现的,而RPC框架反序列化出traceid后就会将其放入到ThreadLocal中,这样在模块的各个阶段记录log时都可以通过该ID标识出该请求。并且随着微服务框架的完成,也可以通过一个服务将traceid串联起来,分析一次请求中各个阶段的状态以及耗时。
    requestid
  • 作为db的本地缓存,通过ThreadLocal-redis-MySQL三级关系,ThreadLocal作为生命周期最短的缓存,缓存查询的结果,对于在同一个线程中的同样的查询,能够快速返回

ThreadLocal的实现原理

ThreadLocal.get()

ThreadLocal实现结构以及执行的过程如下图所示。

ThreadLocal的几个关键词。

  1. 哈希,每个线程内部独立维护着一个ThreadLocalMap,这是一个Entry[]数组,通过对ThreadLocal进行hash(具体细节读者可以从源码了解)获取到Entry的下标
  2. 哈希冲突的解决办法采用了开放地址法,对于如图所示hash冲突的情况则下标挪一位再找(哈希冲突的三种解决办法:HashMap常用的拉链法、开放地址法、再哈希法,感兴趣的读者可以自行搜索:哈希冲突的解决办法)。ThreadLocal通常存放的数据量不会特别大,并且使用开放地址法(或叫开放寻址法)相对于拉链法而言节省了存储指针的空间
  3. WeakReference弱引用,ThreadLocalMap中对于ThreadLocal的引用使用了弱引用,弱引用的作用是当该引用是该对象的唯一一个引用时,不阻碍GC的回收,下面将展开讨论下ThreadLocal中弱引用与内存泄漏的问题

ThreadLocalMap中的弱引用与使用注意

如前文所述,ThreadLocalMap其实是一个ThreadLocal –> value的映射,具体的实现关系如下图
ThreaLocal清理的过程
当线程中使用的ThreadLocal置为null的时候,ThreadLocalMap中的弱引用作为最后一个指向ThreadLocal的引用,发生GC的时候直接被回收掉,但是这时Entry中的value不会被回收

ThreadLocal的set/get/remove方法中在遇到key==null的节点时(被称为stale腐烂节点),会进行清理等处理逻辑。

  1. 如果Thread1执行完销毁了,那么ThreadLocalMap会整个销毁,也就不会有内存泄漏的问题了
  2. 如果Thread1长期存在,并且一直在创建新的ThreadLocal,并且从来没有执行过set/get/remove方法是有一定可能导致内存泄漏的
  3. 一般情况下我们会使用线程池,这样会在执行完后表现为线程结束,实际上线程只是回到了池子中等待下次调度的时候再次使用,这种情况时ThreadLocal是会被复用的,假如前面的使用场景中我们使用ThreadLocal保存了traceId,如果线程执行完没有进行回收并且下次执行的时候没有重新设置traceId的话,那么在打印日志的时候又会打印前一次的traceId,这样也会导致很多逻辑上的错误

因此,必须在使用了ThreadLocal的线程执行完后finally中调用threadLocal.remove(),或者如果ThreadLocal<HashMap>的话则调用threadlocal.get().remove()清空HashMap

ThreadLocal的复制

在ThreadLocal的使用中,我们经常会需要创建子线程,希望子线程能够继承父线程的ThreadLocal,还是以traceid的使用场景为例,我们创建了子线程来并发处理耗时的逻辑,并且希望子线程中也能如实的打印当前请求的traceid,但是普通的ThreadLocal在创建新线程后信息会完全丢失,笔者曾经在这里踩到过坑。

所以就需要一种方案来复制ThreadLocal到子线程:

  1. 先将ThreadLocal的内容保存在堆中,再子线程中将堆中的内容复制过来
  2. InheritableThreadLocal(线程池无效),原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中。但是注意!我们现在一般都会使用线程池创建新线程,这种时候所谓的创建新线程只是复用了线程池中已有的线程,并不会调用new Thread()方法,因此使用InheritableThreadLocal往往是没效果的
  3. 阿里巴巴开源了TransmittableThreadLocal,据说可以解决2中的问题,这个后面我们可以再看下,笔者一般只是使用1的方法基本就可以解决子线程ThreadLocal复制的问题

reference

[1] ThreadLocal-hash冲突与内存泄漏

[2] ThreadLocal面试攻略:吃透它的每一个细节和设计原理

[3] 面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

本文转载自: 掘金

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

【译】 Syslog:系统管理员完整指南 Syslog:系统

发表于 2020-08-07
  • 原文地址:Syslog : The Complete System Administrator Guide
  • 原文作者:Schkn
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:githubmnume
  • 校对者:todaycoder001, shixi-li, portandbridge

Syslog:系统管理员完整指南

Syslog: The Complete System Administrator Guide

如果你是系统管理员,或者只是一个普通的 Linux 用户,那么你很有可能至少使用过一次 Syslog。

在你的 Linux 系统上,几乎所有与系统日志相关的东西都与 Syslog 协议有关。

协议由埃里克·奥尔曼(伯克利大学)在 80 年代早期设计,它是一个规范,定义了任何系统上消息记录的标准。

是的……任何系统。

Syslog 并不依赖 Linux 操作系统,它也可以在 Windows 操作系统上使用,或者在任何实现 syslog 协议的操作系统上使用。

如果你想更多地了解 syslog 和通常的 Linux 日志记录,这可能是你应该阅读的教程。

以下是你需要了解的关于 syslog 的所有信息。

I – Syslog 的目的是什么?

Syslog presentation card

Syslog 是生成、转发和收集在 Linux 实例上生成日志的标准。Syslog 定义了严重性级别和设施级别,有助于用户更好地理解其计算机上生成的日志。日志稍后可以在部署 Syslog 协议的服务器上分析和展示。

以下是 syslog 协议最初设计的几个原因:

  • 定义体系结构:稍后将详细解释这一点,但是如果 syslog 是一个协议,它可能是具有多个客户端和服务器的完整网络体系结构的一部分。因此,我们需要定义角色,简而言之:你是接收、生成还是转发数据?
  • 消息格式:syslog 定义了消息的格式化方式。这显然需要标准化,因为日志经常被解析并存储到不同的存储引擎中。因此,我们需要定义 syslog 客户端能够产生什么,syslog 服务器能够接收什么;
  • 指定可靠性:syslog 需要定义它如何处理无法传递的消息。作为 TCP/IP 堆栈的一部分,syslog 显然会在底层网络协议(TCP 或 UDP)上被选择;
  • 处理身份验证或消息真实性:syslog 需要一种可靠的方法来确保客户端和服务器以安全的方式进行交互,并且接收到的消息不会被更改。

现在我们知道最初为什么制定 Syslog,让我们看看 Syslog 架构是如何工作的。

II – 什么是 Syslog 架构?

当设计一个日志架构时,比如一个集中式日志服务器,很可能多个实例会一起工作。

有些实例将生成日志消息,它们将被称为“设备”或 “syslog 客户端”。

有些只是转发收到的消息,它们将被称为“中继”。

最后,在某些情况下,你将接收和存储日志数据,这些被称为“收集器”或 “syslog 服务器”。

Syslog architecture components

了解这些概念后,我们可以说独立的 Linux 计算机本身就充当了 “syslog 客户端 - 服务器”:它生成日志数据,由 rsyslog 收集并存储到文件系统中。

这里有一组围绕这一原则的架构示例。

在第一种设计中,你有一个设备和一个收集器。这是最简单的日志架构形式。

One device and one collector

在你的基础架构中添加一些更多的客户端,你就拥有了集中式日志架构的基础。

Multiple devices and one collector

多个客户端正在生成数据,并将其发送到负责聚合和存储客户端数据的集中式 syslog 服务器。

如果我们要复杂化我们的架构,我们可以添加一个“中继”。

例如,中继可以是 Logstash 实例,但在客户端也可以是 rsyslog 规则。

Multiple devices, one collector and one relay

这些中继大多充当“基于内容的路由器”(如果你不熟悉基于内容的路由器,这里有一个链接便于你理解它)。
这意味着基于日志内容,数据将被重定向到不同的位置。如果你对数据不感兴趣,也可以将其完全丢弃。

现在我们已经有了详细的 Syslog 组件,让我们看看 Syslog 消息是什么样子的。

III – Syslog 消息格式是什么?

Syslog 格式分为三个部分:

  • PRI 部分: 详细说明消息优先级(从调试消息到紧急事件)以及设施级别(邮件、授权、内核);
  • HEADER 部分: 由时间戳和主机名两个字段组成,主机名是发送日志的计算机名;
  • MSG 部分: 该部分包含发生事件的实际信息。它也分为 TAG 和 CONTENT 字段。

Syslog format explained

在详细描述 syslog 格式的不同部分之前,让我们快速了解 syslog 的严重性级别以及系统日志设施级别。

a – 什么是 Syslog 设施级别?

简单来说,设施级别用于确定生成日志的程序或系统的一部分。

默认情况下,系统的某些部分会被赋予功能级别,例如使用 kern 功能的内核,或者使用邮件功能的邮件系统。

如果第三方想要记录日志,它可能会保留一组从 16 到 23 的设施级别,称为**“本地使用”设施级别**。

或者,他们可以使用“用户级别”工具,这意味着他们可以记录与执行命令的用户相关的日志。

简而言之,如果我的 Apache 服务器由 “apache” 用户运行,那么日志将存储在一个名为 “apache.log” 的文件中(.log)

下表描述了 Syslog 设施级别:

Numerical Code Keyword Facility name
0 kern Kernel messages
1 user User-level messages
2 mail Mail system
3 daemon System Daemons
4 auth Security messages
5 syslog Syslogd messages
6 lpr Line printer subsystem
7 news Network news subsystem
8 uucp UUCP subsystem
9 cron Clock daemon
10 authpriv Security messages
11 ftp FTP daemon
12 ntp NTP subsystem
13 security Security log audit
14 console Console log alerts
15 solaris-cron Scheduling logs
16-23 local0 to local7 Locally used facilities

这些级别你是不是很眼熟?

是的!在 Linux 系统中,默认情况下,文件由设施名称分隔,这意味着你将有一个用于身份验证的文件(auth.log),一个用于内核的文件(kern.log)等等。

这是我的 Debian 10 实例的截屏示例.

展示 debian 10 上的设施日志

现在我们已经看到了 syslog 设施级别,让我们来描述什么是 syslog 严重性级别。

b – Syslog 严重性级别是什么?

Syslog 严重级别用于事件的严重程度,范围从调试、信息消息到紧急级别。

与 Syslog 设施级别相似,严重性级别分为 0 到 7 的数字类别,0 是最紧急的紧急级别。

下表中描述的是 syslog 严重性级别:

Value Severity Keyword
0 Emergency emerg
1 Alert alert
2 Critical crit
3 Error err
4 Warning warning
5 Notice notice
6 Informational info
7 Debug debug

即使默认情况下日志是按设施名称存储的,你也可以完全按事件的严重性级别来存储它们。

如果你使用 rsyslog 作为默认系统日志服务器,你可以检查 rsyslog 属性 配置日志的分隔方式。

现在你对设施和严重性有了更多的了解,让我们回到 syslog 消息格式。

c – PRI 部分是什么?

PRI 块是 syslog 格式消息的第一部分。

PRI 在尖括号之间存储“优先级值”。

还记得你刚刚学到的设施和严重程度吗?

如果你使用消息设施号,将其乘以 8,并加上严重性级别,你将获得 syslog 消息的“优先级值”。

如果你希望将来解码你的 syslog 消息,请记住这一点。

d – HEADER 部分是什么?

如前所述,HEADER 部分由两个关键信息组成: TIMESTAMP 部分和 HOSTNAME 部分(有时可以解析为一个 IP 地址)

该 HEADER 部分直接连着 PRI 部分,正好在右尖括号之后。

值得注意的是 TIMESTAMP 部分的格式是 “Mmm dd hh:mm:ss” 格式,“Mmm” 是一年中一个月的前三个字母。

HEADER part examples

谈到 HOSTNAME ,它通常是在你键入 HOSTNAME 命令时给出的。如果找不到,将为其分配主机的 IPv4 或 IPv6。

Hostname on Debian 10

IV – Syslog 消息传递是如何工作的?

发布 Syslog 消息时,你需要确保使用可靠和安全的方式来传递日志数据。

Syslog 在这方面当然也自有一套想法,下面是这些问题的一些解答。

a – syslog 转发是什么?

Syslog 转发包括将客户端日志发送到远程服务器,以便对其进行集中记录,从而使日志分析和可视化更加容易。

大多数情况下,系统管理员不是监控一台机器,而是需要现场和远程监控几十台机器。

因此,使用不同的通信协议(如 UDP 或 TCP)将日志发送到称为集中式日志服务器的远程机器是一种非常常见的做法。

b – Syslog 使用 TCP 还是 UDP ?

根据RFC 3164规范的规定,syslog 客户端使用 UDP 向系统日志服务器发送消息。

此外,Syslog 使用端口 514 进行 UDP 通信。

但是,在最近的 syslog 实现中,例如 rsyslog 或 syslog-ng,你可以使用 TCP (Transmission Control Protocol) 作为安全的通信通道。

例如,rsyslog 使用端口 10514 进行 TCP 通信来确保传输链路中没有数据包丢失。

此外,你可以基于 TCP 使用 TLS/SSL 协议来加密系统日志数据包,确保不会出现中间人攻击来监视你的日志。

如果你对 rsyslog 感兴趣,这里有一个关于如何以安全可靠的方式设置一个完整的集中式日志服务器的教程。

V – 当前的 Syslog 实现有哪些?

Syslog 是一个规范,但不是 Linux 系统中的实际实现。

以下是 Linux 上当前 Syslog 实现的列表:

  • Syslog daemon:发布于 1980 年,syslog 守护程序可能是第一个实现,并且只支持有限的一组功能(如 UDP 传输)。它通常被称为 Linux 上的 sysklogd 守护程序;
  • Syslog-ng:syslog-ng 于 1998 年发布,它扩展了原始 syslog daemon 的功能集,包括 TCP 转发(从而增强了可靠性)、TLS 加密和基于内容的过滤器。你还可以将日志存储到本地数据库中以供进一步分析。

Syslog-ng演示卡片

  • Rsyslog:rsyslog 于 2004 年由 Rainer Gerhards 发布,是大多数实际 Linux 发行版( Ubuntu、RHEL、Debian 等)上的默认 syslog 实现)。它提供了与 syslog-ng 相同的转发功能,但是它允许开发人员从更多的来源(例如 Kafka、文件或者 Docker)中选择数据

 Rsyslog 演示卡片

VI – 什么是日志最佳实践?

在操作系统日志或构建完整的日志架构时,你需要了解一些最佳实践:

  • 除非你愿意丢失数据,否则请使用可靠的通信协议。 在 UDP(一种不可靠的协议)和 TCP(一种可靠的协议)之间进行选择真的很重要。提前做出这个选择;
  • 使用 NTP 协议配置你的主机: 当你想要使用实时日志调试时,最好让主机同步,否则很难准确调试事件;
  • 保护好你的日志: 使用 TLS/SSL 协议肯定会对你的实例产生一些性能影响,但是如果你要转发身份验证或内核日志,最好对它们进行加密,以确保没有人能够访问关键信息;
  • 你应该避免过度记录: 定义好的日志策略对你的公司至关重要。例如,你必须决定你是否有兴趣存储(并且基本上消耗带宽)信息日志或调试日志。例如,你可能只对错误日志感兴趣;
  • 定期备份日志数据: 如果你关注保留敏感日志,或者如果你定期接受审计,你可能在有关的外部驱动器或正确配置的数据库上备份日志;
  • 设置日志保留策略: 如果日志太旧,你可能会有兴趣丢弃它们,也称为“轮换”它们。该操作是通过 Linux 系统上的 logrotate 实用程序完成的。

VII – 结论

对于愿意深入了解服务器中的日志功能如何运作的系统管理员或 Linux 工程师来说,syslog 协议绝对是经典之作。

然而,有理论的时候,也有实践的时候。

那么你应该做什么?你有多种选择。

你可以从在你的实例上设置syslog 服务器开始,例如 Kiwi Syslog 服务器,并开始从中收集数据。

或者,如果你有更大的基础架构,你可能应该首先建立 集中式日志体系结构, 然后使用非常现代的工具如 Kibana 可视化工具对其进行监控。

我希望你今天学到了一些东西。

活在当下,一如既往地享受乐趣。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文转载自: 掘金

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

阿里双十一的性能凶手之一:自定义异常为什么性能差(求点赞)

发表于 2020-08-06

IT 老哥

一个在大厂做高级Java开发的程序猿

关注微信公众号:IT 老哥

回复:Java实战项目视频教程:即可获取200G,27套实战项目视频教程

回复:Java 学习路线,即可获取最新最全的一份学习路线图

回复:Java 电子书,即可领取 13 本顶级程序员必读书籍

回复:Java 全套教程,即可领取:Java 基础、Java web、JavaEE 全部的教程,包括 spring boot 等

回复:简历模板,即可获取 100 份精美简历

老哥哔哔叨

大家应该都经历过双十一吧,那个流量大的恐怖吧,那个并发高的吓人吧。那么在一个高并发的系统里,有哪些点是影响系统性能的呢,今天我们来讲其中一个点:自定义异常

疯狂的异常

为什么异常会影响性能

首先给大家看一段JDK的Throwable源码

1
2
3
4
5
6
7
8
复制代码public synchronized Throwable fillInStackTrace() {
    if (stackTrace != null ||
        backtrace != null /* Out of protocol state */ ) {
        fillInStackTrace(0);
        stackTrace = UNASSIGNED_STACK;
    }
    return this;
  }

上面这段JDK的源码就是抛出异常时会调用的方法,这段方法暴露出两个问题

  • 使用了synchronized修饰整个异常方法
  • 将异常追踪信息放到了堆栈中(想想JVM和线程)

异常种类

  • 业务异常

这些是我们自定义的、可以预知的异常,抛出这种异常并不表示系统出了问题,而是正常业务逻辑上的需要,例如用户名密码错误、参数错误等。

  • 系统异常

往往是运行时异常,比如数据库连接失败、IO 失败、空指针等,这种异常的产生多数表示系统存在问题,需要人工排查定位。

相信大家都接触过异常,对于业务异常,我们只需要简单的知道一个描述问题的字符串即可,栈追踪信息对我们的意义并不大。而对于系统异常,追踪信息才是排查错误不可或缺的参考。

大家试想,如果前端传的参数错了,系统里就抛出一个异常,那么在双十一的情况下一秒钟得抛出多少个异常呢?

问题思考

  • 抛异常的时候是不是会被 synchronized 上同步锁?
  • 需不需要线程去执行?
  • 是不是得创建异常对象?
  • 需不需要堆栈去存储?
  • 需不需要 jvm 去垃圾回收?

性能测试

  • 创建普通 Java 对象 (CustomObject extends HashMap)
  • 创建普通 Java 异常对象(CustomException extends Exception)
  • 创建改进的 Java 业务异常对象 (CustomException extends Exception,覆写 fillInStackTrace 方法,并且去掉同步)

测试结果

(运行环境:xen 虚拟机,5.5G 内存,8 核;jdk1.6.0_18)

(10 个线程,创建 10000000 个对象所需时间)

  • 普通 Java 对象:45284 MS
  • 普通 java 异常:205482 MS
  • 改进的 Java 业务异常:16731 MS

大家可以看到正常抛出 Exception 的和覆写了 fillInStackTrace 的 Exception,性能差距了很多倍,如果高并发的系统里,就像雪球一样越滚越大。影响系统的并发量。

解决方案:覆写 fillInStackTrace

我们来看看非常 NB 的kafka源码是如何优化的。

1
2
3
4
5
6
复制代码/* avoid the expensive and useless stack trace for api exceptions */
/* 翻译:避免对api异常进行昂贵且无用的堆栈跟踪 */
@Override
public Throwable fillInStackTrace() {
    return this;
}

很多开源框架都是这样处理,避免不必要的性能浪费。

老哥结语

什么是匠人精神,就是将一件事情做到极致。优化永无止境,且行且珍惜。

本文转载自: 掘金

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

从零搭建Spring Boot脚手架(2):增加通用的功能

发表于 2020-08-06

  1. 前言

今天开始搭建我们的kono Spring Boot脚手架,首先会集成Spring MVC并进行定制化以满足日常开发的需要,我们先做一些刚性的需求定制,后续再补充细节。如果你看了本文有什么问题可以留言讨论。多多持续关注,共同学习,共同进步。

Gitee: gitee.com/felord/kono

GitHub: github.com/NotFound403…

  1. 统一返回体

在开发中统一返回数据非常重要。方便前端统一处理。通常设计为以下结构:

1
2
3
4
5
6
7
8
9
json复制代码{
"code": 200,
"data": {
"name": "felord.cn",
"age": 18
},
"msg": "",
"identifier": ""
}
  • code 业务状态码,设计时应该区别于http状态码。
  • data 数据载体,用以装载返回给前端展现的数据。
  • msg 提示信息,用于前端调用后返回的提示信息,例如 “新增成功”、“删除失败”。
  • identifier 预留的标识位,作为一些业务的处理标识。

根据上面的一些定义,声明了一个统一返回体对象RestBody<T>并声明了一些静态方法来方便定义。

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
java复制代码package cn.felord.kono.advice;

import lombok.Data;

import java.io.Serializable;

/**
* @author felord.cn
* @since 22:32 2019-04-02
*/
@Data
public class RestBody<T> implements Rest<T>, Serializable {

private static final long serialVersionUID = -7616216747521482608L;
private int code = 200;
private T data;
private String msg = "";
private String identifier = "";


public static Rest<?> ok() {
return new RestBody<>();
}

public static Rest<?> ok(String msg) {
Rest<?> restBody = new RestBody<>();
restBody.setMsg(msg);
return restBody;
}

public static <T> Rest<T> okData(T data) {
Rest<T> restBody = new RestBody<>();
restBody.setData(data);
return restBody;
}

public static <T> Rest<T> okData(T data, String msg) {
Rest<T> restBody = new RestBody<>();
restBody.setData(data);
restBody.setMsg(msg);
return restBody;
}


public static <T> Rest<T> build(int code, T data, String msg, String identifier) {
Rest<T> restBody = new RestBody<>();
restBody.setCode(code);
restBody.setData(data);
restBody.setMsg(msg);
restBody.setIdentifier(identifier);
return restBody;
}

public static Rest<?> failure(String msg, String identifier) {
Rest<?> restBody = new RestBody<>();
restBody.setMsg(msg);
restBody.setIdentifier(identifier);
return restBody;
}

public static Rest<?> failure(int httpStatus, String msg ) {
Rest<?> restBody = new RestBody< >();
restBody.setCode(httpStatus);
restBody.setMsg(msg);
restBody.setIdentifier("-9999");
return restBody;
}

public static <T> Rest<T> failureData(T data, String msg, String identifier) {
Rest<T> restBody = new RestBody<>();
restBody.setIdentifier(identifier);
restBody.setData(data);
restBody.setMsg(msg);
return restBody;
}

@Override
public String toString() {
return "{" +
"code:" + code +
", data:" + data +
", msg:" + msg +
", identifier:" + identifier +
'}';
}
}

但是每次都要显式声明返回体也不是很优雅的办法,所以我们希望无感知的来实现这个功能。Spring Framework正好提供此功能,我们借助于@RestControllerAdvice和ResponseBodyAdvice<T>来对项目的每一个@RestController标记的控制类的响应体进行后置切面通知处理。

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
java复制代码/**
* 统一返回体包装器
*
* @author felord.cn
* @since 14:58
**/
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}

@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// 如果为空 返回一个不带数据的空返回体
if (o == null) {
return RestBody.ok();
}
// 如果 RestBody 的 父类 是 返回值的父类型 直接返回
// 方便我们可以在接口方法中直接返回RestBody
if (Rest.class.isAssignableFrom(o.getClass())) {
return o;
}
// 进行统一的返回体封装
return RestBody.okData(o);
}
}

当我们接口返回一个实体类时会自动封装到统一返回体RestBody<T>中。

既然有ResponseBodyAdvice,就有一个RequestBodyAdvice,它似乎是来进行前置处理的,以后可能有一些用途。

  1. 统一异常处理

统一异常也是@RestControllerAdvice能实现的,可参考之前的Hibernate Validator校验参数全攻略。这里初步集成了校验异常的处理,后续会添加其他异常。

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
java复制代码/**
* 统一异常处理
*
* @author felord.cn
* @since 13 :31 2019-04-11
*/
@Slf4j
@RestControllerAdvice
public class ApiExceptionHandleAdvice {

@ExceptionHandler(BindException.class)
public Rest<?> handle(HttpServletRequest request, BindException e) {
logger(request, e);
List<ObjectError> allErrors = e.getAllErrors();
ObjectError objectError = allErrors.get(0);
return RestBody.failure(700, objectError.getDefaultMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public Rest<?> handle(HttpServletRequest request, MethodArgumentNotValidException e) {
logger(request, e);
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
ObjectError objectError = allErrors.get(0);
return RestBody.failure(700, objectError.getDefaultMessage());
}

@ExceptionHandler(ConstraintViolationException.class)
public Rest<?> handle(HttpServletRequest request, ConstraintViolationException e) {
logger(request, e);
Optional<ConstraintViolation<?>> first = e.getConstraintViolations().stream().findFirst();
String message = first.isPresent() ? first.get().getMessage() : "";
return RestBody.failure(700, message);
}


@ExceptionHandler(Exception.class)
public Rest<?> handle(HttpServletRequest request, Exception e) {
logger(request, e);
return RestBody.failure(700, e.getMessage());
}


private void logger(HttpServletRequest request, Exception e) {
String contentType = request.getHeader("Content-Type");
log.error("统一异常处理 uri: {} content-type: {} exception: {}", request.getRequestURI(), contentType, e.toString());
}
}
  1. 简化类型转换

简化Java Bean之间转换也是一个必要的功能。 这里选择mapStruct,类型安全而且容易使用,比那些BeanUtil要好用的多。但是从我使用的经验上来看,不要使用mapStruct提供的复杂功能只做简单映射。详细可参考文章Spring Boot 2 实战:集成 MapStruct 类型转换。

集成进来非常简单,由于它只在编译期生效所以引用时的scope最好设置为compile,我们在kono-dependencies中加入其依赖管理:

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>

在kono-app中直接引用上面两个依赖,但是这样还不行,和lombok一起使用编译容易出现SPI错误。我们还需要集成相关的Maven插件到kono-app编译的生命周期中去。参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<showWarnings>true</showWarnings>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

然后我们就很容易将一个Java Bean转化为另一个Java Bean。下面这段代码将UserInfo转换为UserInfoVO而且自动为UserInfoVO.addTime赋值为当前时间,同时这个工具也自动注入了Spring IoC,而这一切都发生在编译期。

编译前:

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* @author felord.cn
* @since 16:09
**/
@Mapper(componentModel = "spring", imports = {LocalDateTime.class})
public interface BeanMapping {

@Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
UserInfoVO toUserInfoVo(UserInfo userInfo);

}

编译后:

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
java复制代码package cn.felord.kono.beanmapping;

import cn.felord.kono.entity.UserInfo;
import cn.felord.kono.entity.UserInfoVO;
import java.time.LocalDateTime;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-07-30T23:11:24+0800",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_252 (AdoptOpenJDK)"
)
@Component
public class BeanMappingImpl implements BeanMapping {

@Override
public UserInfoVO toUserInfoVo(UserInfo userInfo) {
if ( userInfo == null ) {
return null;
}

UserInfoVO userInfoVO = new UserInfoVO();

userInfoVO.setName( userInfo.getName() );
userInfoVO.setAge( userInfo.getAge() );

userInfoVO.setAddTime( LocalDateTime.now() );

return userInfoVO;
}
}

其实mapStruct也就是帮我们写了Getter和Setter,但是不要使用其比较复杂的转换,会增加学习成本和可维护的难度。

  1. 单元测试

将以上功能集成进去后分别做一个单元测试,全部通过。

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
java复制代码    @Autowired
MockMvc mockMvc;
@Autowired
BeanMapping beanMapping;

/**
* 测试全局异常处理.
*
* @throws Exception the exception
* @see UserController#getUserInfo()
*/
@Test
void testGlobalExceptionHandler() throws Exception {

String rtnJsonStr = "{\n" +
" \"code\": 700,\n" +
" \"data\": null,\n" +
" \"msg\": \"test global exception handler\",\n" +
" \"identifier\": \"-9999\"\n" +
"}";

mockMvc.perform(MockMvcRequestBuilders.get("/user/get"))
.andExpect(MockMvcResultMatchers.content()
.json(rtnJsonStr))
.andDo(MockMvcResultHandlers.print());
}

/**
* 测试统一返回体.
*
* @throws Exception the exception
* @see UserController#getUserVO()
*/
@Test
void testUnifiedReturnStruct() throws Exception {
// "{\"code\":200,\"data\":{\"name\":\"felord.cn\",\"age\":18,\"addTime\":\"2020-07-30T13:08:53.201\"},\"msg\":\"\",\"identifier\":\"\"}";
mockMvc.perform(MockMvcRequestBuilders.get("/user/vo"))
.andExpect(MockMvcResultMatchers.jsonPath("code", Is.is(200)))
.andExpect(MockMvcResultMatchers.jsonPath("data.name", Is.is("felord.cn")))
.andExpect(MockMvcResultMatchers.jsonPath("data.age", Is.is(18)))
.andExpect(MockMvcResultMatchers.jsonPath("data.addTime", Is.is(notNullValue())))
.andDo(MockMvcResultHandlers.print());
}


/**
* 测试 mapStruct类型转换.
*
* @see BeanMapping
*/
@Test
void testMapStruct() {
UserInfo userInfo = new UserInfo();
userInfo.setName("felord.cn");
userInfo.setAge(18);
UserInfoVO userInfoVO = beanMapping.toUserInfoVo(userInfo);

Assertions.assertEquals(userInfoVO.getName(), userInfo.getName());
Assertions.assertNotNull(userInfoVO.getAddTime());
}
  1. 总结

自制脚手架初步具有了统一返回体、统一异常处理、快速类型转换,其实参数校验也已经支持了。后续就该整合数据库了,常用的数据库访问技术主要为Mybatis、Spring Data JPA、JOOQ等,不知道你更喜欢哪一款?欢迎留言讨论。

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

本文转载自: 掘金

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

1…788789790…956

开发者博客

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