
阿里云优惠码阿里云推荐券bieryun.com
MySQL8.0.28安装教程全程参考MySQL官方文档前言为了MySQL8.0.28安装教程我竟然在MySQL官方文档逛了一天,至此献给想入门MySQL8.0的初学者。以目前最新版本的MySQL8.0.28为示例进行安装与初步使用的详细讲解,面向初学者的详细教程。无论是Windows还是Linux上安装,咱都会。这也许是迄今为止全网最最最详细的MySQL8.0.28的安装与使用教程。温故而知新,可以为师矣。咱缺的是学习的途径吗?答案并不是,而是缺乏学习的方法。官方文档是很详细的,而且是权威的,其它的那些书籍、博文啊都是基于官方文档以及自己的使用经验总结的。写这篇文章的用意,希望大家可以总结自己的学习方法,善于利用官方文档来提升自己。建议初学者多在命令行窗口下进行练习,熟能生巧。达到一定的熟练程度,再借助客户端工具提高我们的工作效率。最终目的是啥?活下去呗,提高捞金能力。当然开个玩笑,回到正题,接着往下看。从下载到安装,再到忘记密码解决方案。一步步使用命令行窗口学会基本操作,然后使用客户端远程连接工具。最后配合时下比较火热的Java语言进行演示如何使用JDBC连接最新的MySQL8.0数据库,以及执行查询返回结果。正文咱也不多哔哔,直接上干货,要的就是实用性和性价比!一、MySQL8.0.28下载可以下载msi文件一键安装或者解压版zip文件进行命令行初始化安装。MySQL官网下载地址https://dev.mysql.com/downloads/mysql/1、Windows版本下载在Windows下可以选择下载msi文件或者解压版zip文件。一般使用,选择我使用紫色框线选中的即可。关于下面的Debug Test Suite,是带有许多的测试套件在里面,对于有测试需求的人员可以进行下载。2、Linux版本下载根据自己需要的Linux发行版版本适配的MySQL进行选择。比如,我个人选择的是自己比较熟悉的Redhat7系列进行下载。同样有bundle版本,包含了一些插件和依赖在里面,便于使用rpm包安装。安装单个的server服务,需要安装其它的依赖包比较繁琐。对于初学者,建议直接下载RPM bundle版本。我偏不,就要折腾。那也行,请接着往下看,一样提供了详细安装步骤。3、注意事项一般人可以能没仔细看,官方会提示登录,加了button按钮字体非常显眼,而下面的的我需要立即下载则字体很小。所以注意了,选择下面的No thanks,我需要立即下载。选择的是社区版本,免费提供下载。二、MySQL8.0安装初学者尝鲜,建议在Windows下安装。一般情况,默认安装一个MySQL版本服务实例。也不排除预算有限,在同台服务器上安装多个实例进行测试。默认端口3306,如果在发布(生产)环境建议修改默认端口,达到不让别人一下就猜到的目的。接下来安装测试多个MySQL服务版本共存一个操作系统下,只针对于Windows下安装多个服务(没有使用虚拟机工具,真机环境下测试)。Linux下有便捷的yum源以及apt方式安装,一键安装所需依赖,但也有比较繁琐的rpm包安装。1、Windows下安装配置环境变量,编辑系统环境变量,控制面板>所有控制面板项>系统>高级系统配置>系统环境变量:#变量名 MySQL_HOME #变量值 D:\work\mysql-8.0.28-winx64\binmsi文件安装就不介绍了,傻瓜式的一键安装,注意选择路径。主要介绍解压版本zip的安装与实例化:mysql-8.0.28-winx64.zip1.1、解压上面准备的安装包mysql-8.0.28-winx64.zip在解压后的D:\work\mysql-8.0.28-winx64目下新增my.ini文件,默认解压版是没有的。然后加入如下配置:[client] # 设置mysql客户端默认字符集 default-character-set=utf8 [mysqld] # 设置3307端口,有多个服务,为了不冲突修改默认的3306端口为3307 port=3307 # 设置mysql的安装目录 basedir=D:\\work\\mysql-8.0.28-winx64 # 设置 mysql数据库存放目录 datadir=D:\\work\\mysql-8.0.28-winx64\\data # 允许最大连接数 max_connections=20 # 服务端使用的字符集默认为8比特编码的latin1字符集 character-set-server=utf8 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB1.2、实例化以管理员身份运行CMD命令窗口,切换到mysql解压后的目录下:-- 第一步执行d:,切换到D盘 d: -- 第二步执行cd命令,切换到个人安装mysql的bin目录下 cd D:\work\mysql-8.0.28-winx64\设置为空密码,去掉不必要的麻烦。mysqld --initialize-insecure1.3、安装服务进入到解压后MySQL的bin目录下,执行安装服务命令:cd D:\work\mysql-8.0.28-winx64\bin mysqld install mysqld install --service -mysql8如果没有安装多个服务,使用mysqld install即可。可以不用指定服务名,默认的服务名为MySQL。1.4、服务命令使用,需要管理员身份运行CMD命令,注意看我的路径是在bin目录下执行的我没有配置MySQL8的系统环境变量,所以都在MySQL的bin目录中执行命令。启动服务net start mysql# 启动服务 net start mysql D:\work\mysql-8.0.28-winx64\bin>net start mysql8 MySQL8 服务正在启动 . MySQL8 服务已经启动成功。停止服务net stop mysql# 停止服务 net stop mysql D:\work\mysql-8.0.28-winx64\bin>net stop mysql8 MySQL8 服务正在停止.. MySQL8 服务已成功停止。删除服务sc delete mysql# 删除服务 sc delete mysql # 或者mysqld remove删除服务,需要在mysql的bin目录下执行mysqld命令 mysqld remove mysql1.5、注意事项# 安装服务指定了服务名为MySQL8 计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MySQL8 # 或者是MySQL 计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MySQL安装服务指定了服务名为MySQL8。在下面的多实例服务共存也提到过,需要将原始残留的注册表删掉,重启电脑,再进行安装即可。出现了丢失MSVCR120.dll,缺少组件,安装以下组件解决vcredist_x64.exevcredist_x86.exeDownload Visual C++ Redistributable Packages for Visual Studio 2013 from Official Microsoft Download Center注意:使用了mysqld -initialize,密码是随机生成的,在mysql的错误日志中可以找到例如我的日志(mysql的data中以.err结尾的文件)A temporary password is generated for root@localhost: 6hk20yueza=M修改密码的命令ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'之所以在Windows下介绍的如此详细,是因为我们平时的工作环境更多的是在Windows下进行的。就算使用Linux环境一般也是使用虚拟机配合Linux发行版,再就是云服务器了。MySQL的一些命令都熟悉了,Linux下安装还能难倒你吗?直接翻一翻官方文档即可。2、Linux下安装建议初学者不要像我这样去安装rpm包,你可以直接下载rpm bundle,或者使用mysql官方的yum源。个人有多年Linux使用经验,以及有一定的实际工作经验;知道如何判断哪些包是必须的,以及哪些需要被替换掉。一定要注意Linux操作系统的权限问题,权限在最小范围内满足即可。2.1、准备好安装包直接在官网下载,或者使用wget命令下载都可以,同样可以使用官网的yum源进行安装。亦或是使用apt命令获取安装。至于为什么一些Linux发行版将MySQL从默认中移除了,因为MySQL被Oracle收购后存在闭源的风险。取而代之的是她的妹妹MariaDB,这也是为什么我在安装的时候提到了MariaDB。2.2、安装rpm包系统会提示哪些是需要的依赖包,所以我事先准备需要的依赖包。[mysql@localhost ~]$ rpm -ivh mysql-community-server-8.0.28-1.el7.x86_64.rpm 警告:mysql-community-server-8.0.28-1.el7.x86_64.rpm: 头V4 RSA/SHA256 Signature, 密钥 ID 3a79bd29: NOKEY 错误:依赖检测失败: mysql-community-client(x86-64) >= 8.0.11 被 mysql-community-server-8.0.28-1.el7.x86_64 需要 mysql-community-common(x86-64) = 8.0.28-1.el7 被 mysql-community-server-8.0.28-1.el7.x86_64 需要 mysql-community-icu-data-files = 8.0.28-1.el7 被 mysql-community-server-8.0.28-1.el7.x86_64 需要 [root@localhost mysql]# rpm -ivh mysql-community-icu-data-files-8.0.28-1.el7.x86_64.rpm 警告:mysql-community-icu-data-files-8.0.28-1.el7.x86_64.rpm: 头V4 RSA/SHA256 Signature, 密钥 ID 3a79bd29: NOKEY 准备中... ################################# [100%] 正在升级/安装... 1:mysql-community-icu-data-files-8.################################# [100%] [root@localhost mysql]# rpm -ivh mysql-community-common-8.0.28-1.el7.x86_64.rpm 警告:mysql-community-common-8.0.28-1.el7.x86_64.rpm: 头V4 RSA/SHA256 Signature, 密钥 ID 3a79bd29: NOKEY 准备中... ################################# [100%] 正在升级/安装... 1:mysql-community-common-8.0.28-1.e################################# [100%] [root@localhost mysql]# rpm -ivh mysql-community-client-8.0.28-1.el7.x86_64.rpm 警告:mysql-community-client-8.0.28-1.el7.x86_64.rpm: 头V4 RSA/SHA256 Signature, 密钥 ID 3a79bd29: NOKEY 错误:依赖检测失败: mysql-community-client-plugins = 8.0.28-1.el7 被 mysql-community-client-8.0.28-1.el7.x86_64 需要 mysql-community-libs(x86-64) >= 8.0.11 被 mysql-community-client-8.0.28-1.el7.x86_64 需要2.2.1、安装依赖包,然后使用rpm -qa | grep mysql查询哪些被安装了。怎么传到服务器上,简单一点scp命令即可。rpm -ivh mysql-community-common-8.0.28-1.el7.x86_64 rpm -ivh mysql-community-libs-8.0.28-1.el7.x86_64 rpm -ivh mysql-community-icu-data-files-8.0.28-1.el7.x86_64 rpm -ivh mysql-community-client-8.0.28-1.el7.x86_64 rpm -ivh mysql-community-client-plugins-8.0.28-1.el7.x86_64 [root@localhost mysql]# rpm -qa | grep mysql2.2.2、Redhat7系列需要卸载原有的mariadb-libs,替换为mysql-community-libs依赖$ yum remove mariadb-libs2.2.3、正式安装server$ rpm -ivh mysql-community-server-8.0.28-1.el7.x86_64.rpm查看安装的版本Ver 8.0.28 for Linux on x86_64[mysql@localhost ~]$ mysqladmin --version mysqladmin Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)2.2.4、赋予mysql安装目录所有者为mysql用户,rpm包默认安装后的路径在/var/lib/mysql。在授予mysql用户所有者和所属组权限之后,你可以使用mysql用户进行登录或者启动服务与关闭服务。#添加mysql组 $ groupadd mysql #新增mysql用户到mysql组中 $ useradd -g mysql mysql #修改mysql用户密码 $ mysql passwd #修改所有者 $ chown -R mysql:mysql /var/lib/mysqltips:你也可以将mysql用户加入到/etc/sudoers配置文件中,限制mysql用户使用的权限。2.3、初始化设置密码为空,后续登录可修改密码$ mysqld --initialize-insecure2.4、启动服务与查看服务状态Redhat7系列使用命令启动MySQL服务$ systemctl start mysqld设置开机自启$ systemctl enable mysqld关闭服务$ systemctl stop mysqld重启服务$ systemctl restart mysqld登录mysql$ mysql -uroot -p2.5、设置防火墙加入mysql服务以及需要的端口3306$ firewall-cmd --zone=public --add-port=3306/tcp --permanent $ firewall-cmd --zone=public --add-service=mysql --permanent $ firewall-cmd --reload或者临时关闭防火墙测试$ systemctl stop firewalld.service2.6、测试远程登录开启防火墙,加入了3306/tcp协议规则,加入了mysql服务规则。设置了以前旧的密码缓存验证规则 mysql_native_password,,解决caching_sha2_password验证插件无法被加载的问题。mysql> CREATE USER 'root'@'%' IDENTIFIED BY 'Mysql@123456'; -- 第一步创建用户 mysql> GRANT ALL ON *.* TO root@'%'; -- 第二步授权 mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Mysql@123456'; -- 第三步修改密码验证方式 mysql> flush privileges; -- 第四步刷新权限3、关于忘记密码解决方案很多小伙伴估计都遇到过设置密码后,结果忘记密码了。本文的解决方案,完全适用目前最新版本MySQL8.0.28,亲自测试验证过。参考MySQL8.0官方文档以及stackoverflow解决方法。结果兜兜转转回到了跳过登录密码权限验证,8.0版本之前的方法失效了,咱没跟上MySQL更新的步伐。1、关闭MySQL服务:$ systemctl stop mysqld2、设置MySQL环境选项参数,跳过权限表验证$ systemctl set-environment MYSQLD_OPTS="--skip-grant-tables"或者在/etc/my.cnf文件中添加,是一样的效果。最后记得去掉跳过验证。Windows下在my.ini文件中加入skip-grant-tables。[mysqld] skip-grant-tables3、使用了刚刚的设置启动mysql$ systemctl start mysqld4、登录到root$ mysql -u root5、使用命令更新root用户密码mysql> UPDATE mysql.user SET authentication_string = PASSWORD('MyNewPassword') -> WHERE User = 'root' AND Host = 'localhost'; mysql> FLUSH PRIVILEGES; mysql> quita、这种方式可能行不通UPDATE user SET authentication_string = PASSWORD('123456') WHERE User = 'root' AND Host = 'localhost';b、采取将密码先置空update user set authentication_string = '' where user = 'root';6、修改密码,解决方案,设置更强的密码规则即可As mentioned my shokulei in the comments, for 5.7.6 and later, you should use mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; Or you'll get a warningMySQL8.0出现这种情况,请设置更安全的密码规则比如设置密码为:Mysql@123456,即可成功。ERROR 1819 (HY000): Your password does not satisfy the current policy requirements mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'Mysql@123456';7、关闭mysql服务$ systemctl stop mysqld8、重置前面设置的mysql环境变量参数$ systemctl unset-environment MYSQLD_OPTS9、再次启动mysql$ systemctl start mysqld最终成功登录到mysql$ mysql -u root -p三、MySQL8.0使用主要基于Windows10进行说明的,一些命令同样适用于Linux。1、Windows多个MySQL服务实例共存注意修改注册表路径,解决启动MySQL服务意外停止的情况,提示1067还是1068来着。net start mysql8 net start mysql为了测试演示最新版本,我将服务名改成了MySQL8。# 安装服务指定了服务名为MySQL8 计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MySQL8 # 或者是MySQL 计算机\HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MySQL我之前安装的MariaDB10.5.6。我想继续使用MariaDB,又想体验最新版的MySQL8.0.28,选择这样处理。1.1、登录并指定端口3307,默认为3306,我的MariaDB已经占用了3306端口。个人测试演示多个实例共存改了端口为3307。注意:在Windows下使用cmd命令窗口以管理员身份运行登录,没有配置环境变量也没关系,切换到MySQL安装的bin目录下执行命令。-- 第一步执行d:,切换到D盘 d: -- 第二步执行cd命令,切换到个人安装mysql的bin目录下 cd D:\work\mysql-8.0.28-winx64\bin -- 执行登录命令,并指定端口 mysql -uroot -p -P 3307 -- 查询数据库版本 ```sql mysql> select version(); +-----------+ | version() | +-----------+ | 8.0.28 | +-----------+ 1 row in set (0.00 sec) ```总结一下:第一步执行d:,切换到D盘;第二步执行cd命令,切换到个人安装mysql的bin目录下;第三步执行登录命令,并指定端口登录到mysql;最后进行简单的交互,并查询数据库版本。1.2、初步使用命令行模式进行交互2、权限设置2.1、参考官方文档设置远程登录权限,以及密码校验规则,与安装数据库版本默认使用的默认认证插件有关参考MySQL官方文档:https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password01、Authentication plugin 'caching_sha2_password' is not supported 02、Authentication plugin 'caching_sha2_password' cannot be loaded: dlopen(/usr/local/mysql/lib/plugin/caching_sha2_password.so, 2): image not found 03、Warning: mysqli_connect(): The server requested authentication method unknown to the client [caching_sha2_password]使用上面这种密码缓存验证算法。描述到验证插件不支持caching_sha2_password,不能被加载,服务连接请求提出警告认证方法无法识别客户端。通俗一点解释:在使用SQLyog、MySQL workbench等客户端连接时,密码验证规则是不被允许的。需要更换验证方式,或者其它方式解决验证。下面将会给出解决方案。2.2、修改root用户密码使用alter user语句修改用户密码:ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';2.3、创建普通用户并授权(开发人员或者DBA使用的比较频繁)初学者可以先忽略授权这一步,使用root用户将基本功练扎实。还没入门,就没这头皮发麻的授权模式给弄崩溃了,哈哈。授权命令GRANT,撤销权限命令REVOKE。创建用户并授权参考官方文档:https://dev.mysql.com/doc/refman/8.0/en/roles.html创建角色:CREATE ROLE命令CREATE ROLE 'app_developer', 'app_read', 'app_write';授予角色权限:GRANT命令GRANT ALL ON app_db.* TO 'app_developer'; GRANT SELECT ON app_db.* TO 'app_read'; GRANT INSERT, UPDATE, DELETE ON app_db.* TO 'app_write';创建用户:CREATE USER命令CREATE USER 'dev1'@'localhost' IDENTIFIED BY 'dev1pass'; CREATE USER 'read_user1'@'localhost' IDENTIFIED BY 'read_user1pass'; CREATE USER 'read_user2'@'localhost' IDENTIFIED BY 'read_user2pass'; CREATE USER 'rw_user1'@'localhost' IDENTIFIED BY 'rw_user1pass';授权给创建的用户:GRANT命令GRANT 'app_developer' TO 'dev1'@'localhost'; GRANT 'app_read' TO 'read_user1'@'localhost', 'read_user2'@'localhost'; GRANT 'app_read', 'app_write' TO 'rw_user1'@'localhost';你还可以在my.ini或者my.cnf配置文件中指定设置:[mysqld] mandatory_roles='role1,role2@localhost,r3@%.example.com'同样可以在命令模式下使用SET命令设置:SET PERSIST mandatory_roles = 'role1,role2@localhost,r3@%.example.com';检查角色dev1的权限,检查权限比较多,我就不一一列举。详情可以参考官方文档。mysql> SHOW GRANTS FOR 'dev1'@'localhost'; +-------------------------------------------------+ | Grants for dev1@localhost | +-------------------------------------------------+ | GRANT USAGE ON *.* TO `dev1`@`localhost` | | GRANT `app_developer`@`%` TO `dev1`@`localhost` | +-------------------------------------------------+创建用户并授权,授权全部权限:CREATE USER 'old_app_dev'@'localhost' IDENTIFIED BY 'old_app_devpass'; GRANT ALL ON old_app.* TO 'old_app_dev'@'localhost';锁定用户:锁定:LOCK,盲猜解锁就是UNLOCKALTER USER 'old_app_dev'@'localhost' ACCOUNT LOCK;授权给新的开发账号权限,授权部分权限:CREATE USER 'new_app_dev1'@'localhost' IDENTIFIED BY 'new_password'; GRANT 'old_app_dev'@'localhost' TO 'new_app_dev1'@'localhost';以上提供官方文档进行参考,与其东找找西找找,不如翻阅官方文档更直接更准确。咱缺的不是学习的途径,而是缺乏学习的方法。3、测试创建用户3.1、创建普通用户并授权远程登录创建一个普通用户testmysql> CREATE USER 'test'@'localhost' IDENTIFIED BY '123456'; Query OK, 0 rows affected (0.00 sec)授予用户test在本地(localhost)的权限,只给查询权限(SELECT),授权所有(ALL)mysql> GRANT SELECT ON *.* TO test@'localhost'; Query OK, 0 rows affected (0.01 sec)3.2、给root用户授权创建root用户授权给所有IP都能登录,以及修改密码缓存认证方式。mysql> CREATE USER 'root'@'%' IDENTIFIED BY 'Mysql@123456'; mysql> GRANT ALL ON *.* TO root@'%'; mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Mysql@123456'; mysql> flush privileges;在第三方工具中验证登录结果,在localhost下可以登录成功:目前只给了查询(select)权限,验证插入(insert)权限:mysql> insert into study values(7,'美柑'); ERROR 1142 (42000): INSERT command denied to user 'test'@'localhost' for table 'study'这是在SQLyog工具下进行验证的,建议初学者多在命令行窗口下进行练习,熟能生巧。3.2、授权root用户远程登录,MySQL8.0授权方式用户授权,在MySQL8.0版本中变得更加严格,以前MySQL5.6或者5.7版本中可以执行授权的方式有了变化。经过个人亲测,操作如下。MySQL8.0授权方式,记得使用flush privileges刷新权限mysql> CREATE USER 'root'@'%' IDENTIFIED BY 'Mysql@123456'; -- 第一步创建用户 mysql> GRANT ALL ON *.* TO root@'%'; -- 第二步授权 mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Mysql@123456'; -- 第三步修改密码验证方式 mysql> flush privileges; -- 第四步刷新权限修改密码认证方式(8.0默认使用的是sha2算法缓存认证),第一种解决方案如下,这只是其中一种解决方案,亲测有效。ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';第二种解决方案:在my.ini或者my.cnf配置文件加入如下配置,重启服务并加载配置文件。经过测试没有生效,似乎没有读取到配置文件,但奇怪的是我设置的3307端口和默认存储引擎以及编码格式是生效的。(在官网看到的解决方案)[mysqld] default_authentication_plugin=mysql_native_passwordMySQL8.0官方文档默认设置的认证缓存算法是caching_sha2_passwordALTER USER user(用户) IDENTIFIED WITH caching_sha2_password BY 'password';MySQL8.0之前的授权方式(5.6或者5.7都支持这种方式授权)GRANT ALL PRIVILEGES ON *.* TO '你的用户名'@'你的IP地址' IDENTIFIED BY '设置的密码' WITH GRANT OPTION;示例:授权root户,所有IP都可连接。GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;刷新权限flush privileges;4、如何高效的使用自带官方文档登录到MySQL8,指定3307端口,或者使用默认端口登录。mysql -uroot -p -P 3307 mysql -uroot -p使用帮助命令,以? create contents形式查找系统帮助命令。? create contents; ? create user; ? create database; ? create table; ? select; ? insert; ? update; ? delete; URL: https://dev.mysql.com/doc/refman/8.0/en/select|insert|update|delete.html在使用本地的帮助文档时,你会发现系统自动提示了官方文档的地址 https://dev.mysql.com/doc。示例:查询创建表的帮助命令? create table只展示了一部分内容。mysql> ? create table Name: 'CREATE TABLE' Description: Syntax: CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name (create_definition,...) [table_options] [partition_options] data_type: (see https://dev.mysql.com/doc/refman/8.0/en/data-types.html) index_type: USING {BTREE | HASH} index_option: { KEY_BLOCK_SIZE [=] value | index_type ... } URL: https://dev.mysql.com/doc/refman/8.0/en/create-table.htmlName:查看的帮助命令名称Description:描述Syntax:示例data_type:支持的数据类型index_type:可以使用的索引类型我只列举了部分进行说明,更详细的可以自己测试。在创建用户、数据库以及建表和字段全部采取的大写,因为在Linux和Unix下对大写敏感的,并不是MySQL本身对大小写敏感。1、创建数据库CREATE DATABASE TEST; USE TEST;2、创建表,可以通过ENGINE指定表的存储引擎,mysql5.6以及之后的版本默认为InnoDB存储引擎。CREATE TABLE STUDY( ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT, NAMES VARCHAR(64) NOT NULL )ENGINE=MyISAM;3、插入数据INSERT INTO STUDY VALUES(1,'mysql目前最新版本msyql8.0.28');4、查询数据SELECT * FROM STUDY;5、修改数据UPDATE STUDY S SET S.NAMES='mysql默认的存储引擎是InnoDB' WHERE S.ID=1;6、删除全部数据DELETE FROM STUDY;至此基本的创建用户、创建数据库、增删改查都会使用了。四、MySQL连接工具做了超链接,方便去官网获取。phpMyAdminMYSQL workbenchSQLyog推荐几个比较常用的工具:phpMyAdmin、SQLyog、MySQL Workbench、Navicat可视化工具进行连接操作。工具的使用是其次的,更重要的在于对MySQL命令语句的运用。tips:包含了SQLyog,还整理了部分安装包以及MySQL官方提供的sakila 、world示例哟!链接: https://pan.baidu.com/s/11gIlZKxoTG5BCCcoXdVJRg 提取码: ntu7给出一个使用Navicat逆向生成的示例数据库world的模型:如果真的要使用到建物理模型:推荐你学习Sybase PowerDesigner设计工具的使用,而且需要了解关系数据库设计遵循的三范式。现在数据库设计最多满足3NF,普遍认为范式过高,虽然具有对数据关系更好的约束性,但也导致数据关系表增加而令数据库IO更易繁忙,原来交由数据库处理的关系约束现更多在数据库使用程序中完成。五、MySQL之JDBC1、官方connector-jMySQL8.0的maven安装JDBC:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-installing-maven.htmlJDBC连接驱动管理:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-usagenotes-connect-drivermanager.htmlimport java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; // Notice, do not import com.mysql.cj.jdbc.* // or you will have problems! public class LoadDriver { public static void main(String[] args) { try { // The newInstance() call is a work around for some // broken Java implementations Class.forName("com.mysql.cj.jdbc.Driver").newInstance(); } catch (Exception ex) { // handle the error } } } Connection conn = null; ... try { conn = DriverManager.getConnection("jdbc:mysql://localhost/test?" + "user=root&password=123456"); // Do something with the Connection ... } catch (SQLException ex) { // handle any errors System.out.println("SQLException: " + ex.getMessage()); System.out.println("SQLState: " + ex.getSQLState()); System.out.println("VendorError: " + ex.getErrorCode()); }2、JDBC测试连接MySQL8.0数据库2.1、maven配置设置pom.xml配置文件,使用MySQL最新的版本8.0.28进行连接测试。maven的镜像仓库,可以使用阿里的镜像源地址。<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> <exclusions> <exclusion> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> </exclusion> </exclusions> </dependency>2.2、编写Java代码使用编辑器sts(Spring Tool Suite4或者IDEA)创建普通的maven项目或者springboot项目,然后配置pom.xml。目的:使用纯JDBC测试,或者ORM框架mybatis、JPA、或者hibernate都行,最终达到对数据库进行最基本的增删改查。package com.example.demo.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TestConnMySQL8 { public static void main(String[] args) throws ClassNotFoundException, SQLException { TestSQLConnMySQL(); } private static final Logger log = LoggerFactory.getLogger(TestConnMySQL8.class); //初始化参数 static Connection conn = null; static PreparedStatement ps = null; static ResultSet rs = null; /** * @throws SQLException * @throws ClassNotFoundException */ private static void TestSQLConnMySQL() throws SQLException, ClassNotFoundException { try { Class.forName("com.mysql.cj.jdbc.Driver"); /** * 1.获取连接参数url,username,password,默认端口是3306 * MySQL:url ="jdbc:mysql://127.0.0.1:3306/test"; */ /** MySQL拼接url **/ String url = "jdbc:mysql://192.168.245.147:3306/TEST?useUnicode=true&characterEncoding=utf-8"; String username = "root"; String password = "Mysql@123456"; //获取连接 conn = DriverManager.getConnection(url, username, password); if(null != conn) { log.info("connect database success..."); }else { log.error("connect database failed..."); } //查询数据库 String sql = "SELECT * FROM STUDY"; // 3.通过preparedStatement执行SQL ps = conn.prepareStatement(sql); // 4.执行查询,获取结果集 rs = ps.executeQuery(); // 5.遍历结果集,前提是你的数据库创建了表以及有数据 while (rs.next()) { //对应数据库表中字段类型Int使用getInt,varchar使用getString System.out.println("ID:" + rs.getInt("ID")); System.out.println("姓名:" + rs.getString("NAMES")); } } finally { // 6.关闭连接 释放资源 rs.close(); ps.close(); conn.close(); } } }在sts编辑工具连接并返回测试结果总结以上就是本次MySQL8.0.28安装与使用的全部内容,希望能对你的工作与学习有所帮助。感觉写的好,就拿出你的一键三连。在公众号上更新的可能要快一点,目前还在完善中。能看到这里的,都是帅哥靓妹。如果感觉总结的不到位,也希望能留下您宝贵的意见,我会在文章中进行调整优化。原创不易,转载也请标明出处和作者,尊重原创。不定期上传到github或者gitee。认准龙腾万里sky,如果看见其它平台不是这个ID发出我的文章,就是转载的。MySQL系列文章:《MySQL开发篇,存储引擎的选择真的很重要吗?》已经上传至github和gitee仓库SQL-study。个人github仓库地址,一般会先更新PDF文件,然后再上传markdown文件。如果访问github太慢,可以使用gitee进行克隆。tips:使用hexo搭建的静态博客也会定期更新维护。作者:龙腾万里sky转载地址https://www.cnblogs.com/cnwangk/p/15860688.html
C# 同步 异步 回调 状态机 async await Demo为什么会研究这个?我们项目的客户端和服务端通信用的是WCF,我就想,能不能用异步的方式调用WCF服务呢?或者说能不能用async await的方式调用WCF服务呢?然后我发现WCF是通过BeginXXX和EndXXX这种回调的方式实现异步的,似乎不支持async await语法糖,那只能通过状态机的方式实现了,不然多个请求就会写成回调地狱。如果用Task.Run包起来,里面再写个Wait,这就是假异步,还不如不用异步,按原来非异步的方式写。研究了回调和状态机之后,又看了相关的博客,就像一篇博客里说的,调用WCF不难,异步调用WCF不难,异步调用WCF并保证调用顺序也不难,难的是实现了这些之后,代码的可读性和可维护性。所以我的结论是,调用WCF,如果没有特殊需求,还是不要异步的好,写起来复杂。C# 同步 异步 回调 状态机 async await Demo主要演示了不使用async、await语法糖,通过回调函数和状态机实现异步为什么要写这个Demo?为了帮助理解异步,async、await经过编译生成的状态机代码,有点复杂,这个Demo里写了一个相对简单的状态机代码,便于理解代码说明代码中主要写了三种下载文件的示例同步方式下载文件,为了防止下载较大文件时卡住界面,代码在线程中执行,文件下载完成之前,始终占用一个线程异步方式下载文件,使用了async、await语法糖,下载文件时,可以看到,workerThreads(可用线程数)和completionPortThreads(可用异步线程数)会发生变化,但是不会长时间占用一个线程异步方式下载文件,不使用async、await语法糖,通过回调函数和状态机实现,workerThreads(可用线程数)和completionPortThreads(可用异步线程数)会发生变化,但是不会长时间占用一个线程结论:异步的本质是回调,异步是回调的语法糖相比同步方式,使用异步方式下载文件时,忽略掉误差,下载速度并没有更快,异步的主要优点是不会长时间占用一个线程在没有async、await语法糖时,使用回调函数和状态机也可以实现异步,但代码写的不够优雅,心智负担重,所以async、await的另一个优点是使代码简单异步的本质就是回调,C#异步底层是通过系统级回调和状态机实现的,async、await会被编译成状态机代码,相关于用代码生成器生成了代码,但这个代码自己写的话,心智负担重不使用async、await语法糖的前提下,使用状态机可以避免回调地狱即使不使用async、await语法糖,依然感受到了异步的侵入性,没办法在底层完全封装起来,代码一直侵入到控件的事件里,使用async、await也是,你从代码的底层,写async一直写到控件的事件层思考:经测试,button4_Click、button5_Click、button6_Click在下载大文件时,耗时较长,但是界面并不会卡住,依然可以自由拖动窗体或点击其它按钮button4_Click、button5_Click、button6_Click方法分别调用了AsyncFunctionWithCallback方法AsyncFunctionWithCallback方法调用了HttpUtil类的HttpDownloadFileAsyncWithCallback方法请仔细观察上面的方法,它们没有使用new Thread、Task.Run、Task.Factory.StartNew等线程相关的方法,也就是说执行长时间操作,没有阻塞界面,但是又没有使用线程,实际上下载文件过程中,没有使用C#的CLR线程池,测试时,workerThreads数量不会变化,但是你会看到completionPortThreads数量变化,下载文件的过程使用了异步线程池,但是下载大文件时,并不会长时间占用异步线程池Demo截图测试代码Form1.csusing Models; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Utils; namespace AsyncAwaitDemo { public partial class Form1 : Form { private DateTime _dt1; private System.Timers.Timer _timer; private string _testFileDownloadUrl = "http://desk-fd.zol-img.com.cn/t_s1920x1080c5/g6/M00/06/00/ChMkKWHXqV-IJFpUAEJatCdq_LUAAXW8AA1PvwAQlrM029.jpg?downfile=1642227460763.jpg"; //文件下载测试URL //private string _testFileDownloadUrl = "https://down21.xiazaidb.com/app/xiaomanghe.apk"; //文件下载测试URL //private string _testFileDownloadUrl = "https://codeload.github.com/0611163/DBHelper/zip/refs/heads/master"; //文件下载测试URL //private string _testFileDownloadUrl = "http://down-ww5.537a.com/soft/3/ce/com.yhong.muchun_51982ada.apk"; //文件下载测试URL //private string _testFileDownloadUrl = "https://dl.360safe.com/netunion/20140425/360se+191727+n3f07b78190.exe"; //文件下载测试URL 大一点的文件 public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { //定时器会使用线程池中的一个线程 _timer = new System.Timers.Timer(); _timer.Interval = 100; _timer.Elapsed += _timer_Elapsed; _timer.Start(); } #region Log private void Log(string log) { if (!this.IsDisposed) { string msg = DateTime.Now.ToString("mm:ss.fff") + " " + log + "\r\n\r\n"; if (this.InvokeRequired) { this.BeginInvoke(new Action(() => { textBox1.AppendText(msg); })); } else { textBox1.AppendText(msg); } } } #endregion #region _timer_Elapsed private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { int workerThreads; int completionPortThreads; int maxWorkerThreads; int maxCompletionPortThreads; ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads); this.BeginInvoke(new Action(() => { label1.Text = "程序当前使用线程数:" + (maxWorkerThreads - workerThreads) + " workerThreads:" + workerThreads.ToString() + " completionPortThreads:" + completionPortThreads; })); } #endregion #region button1_Click 测试同步方法 private void button1_Click(object sender, EventArgs e) { Task.Run(() => //下载文件是耗时操作,需要在线程中执行,否则界面卡住 { Log("开始"); DateTime dt = DateTime.Now; //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //方法调用 string result1 = SyncFunction(arg1); string result2 = SyncFunction(arg2); string result3 = SyncFunction(arg3); //输出结果 Log(result1); Log(result2); Log(result3); Log("结束,耗时:" + DateTime.Now.Subtract(dt).TotalSeconds.ToString("0.000")); }); } #endregion #region button2_Click 测试异步方法(三个异步方法并行,按顺序输出) private async void button2_Click(object sender, EventArgs e) { Log("开始"); DateTime dt = DateTime.Now; //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //方法调用 var t1 = AsyncFunction(arg1); var t2 = AsyncFunction(arg2); var t3 = AsyncFunction(arg3); string result1 = await t1; string result2 = await t2; string result3 = await t3; //输出结果 Log(result1); Log(result2); Log(result3); Log("结束,耗时:" + DateTime.Now.Subtract(dt).TotalSeconds.ToString("0.000")); } #endregion #region button3_Click 测试异步方法(三个异步方法顺序执行,按顺序输出) private async void button3_Click(object sender, EventArgs e) { Log("开始"); DateTime dt = DateTime.Now; //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //方法调用 string result1 = await AsyncFunction(arg1); string result2 = await AsyncFunction(arg2); string result3 = await AsyncFunction(arg3); //输出结果 Log(result1); Log(result2); Log(result3); Log("结束,耗时:" + DateTime.Now.Subtract(dt).TotalSeconds.ToString("0.000")); } #endregion #region button4_Click 测试状态机(三个异步方法顺序执行,按顺序输出)(等价于button3_Click) private void button4_Click(object sender, EventArgs e) { List<object> resultList = new List<object>(); //回调、状态机,不使用async await语法糖 Action<int, object> awaitAction = null; awaitAction = (state, result) => { //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //方法调用 if (state == 0) { Log("开始"); _dt1 = DateTime.Now; AsyncFunctionWithCallback(arg1, r => awaitAction(1, r)); return; } if (state == 1) { resultList.Add(result); AsyncFunctionWithCallback(arg2, r => awaitAction(2, r)); return; } if (state == 2) { resultList.Add(result); AsyncFunctionWithCallback(arg3, r => awaitAction(3, r)); return; } //输出结果 if (state == 3) { resultList.Add(result); foreach (object item in resultList) { Log(item != null ? item.ToString() : "null"); } Log("结束,耗时:" + DateTime.Now.Subtract(_dt1).TotalSeconds.ToString("0.000")); return; } }; awaitAction(0, null); } #endregion #region button5_Click 测试状态机(三个异步方法并行,按顺序输出)(等价于button2_Click) private void button5_Click(object sender, EventArgs e) { object[] resultArray = new object[3]; //是否已调用回调函数 bool _completedCalled = false; //锁 object _lock = new object(); //输出结果 Action<object[]> output = arr => { if (arr.Count(a => a != null) == 3) { lock (_lock) { if (!_completedCalled) { _completedCalled = true; foreach (object item in arr) { Log(item != null ? item.ToString() : "null"); } Log("结束,耗时:" + DateTime.Now.Subtract(_dt1).TotalSeconds.ToString("0.000")); } } } }; //回调、状态机,不使用async await语法糖 Action<int, object> awaitAction = null; awaitAction = (state, result) => { //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //方法调用 if (state == 0) { Log("开始"); _dt1 = DateTime.Now; AsyncFunctionWithCallback(arg1, r => awaitAction(1, r)); AsyncFunctionWithCallback(arg2, r => awaitAction(2, r)); AsyncFunctionWithCallback(arg3, r => awaitAction(3, r)); return; } //输出结果 if (state == 1) { resultArray[0] = result; output(resultArray); return; } if (state == 2) { resultArray[1] = result; output(resultArray); return; } if (state == 3) { resultArray[2] = result; output(resultArray); return; } }; awaitAction(0, null); } #endregion #region button6_Click 测试状态机(三个异步方法并行,按顺序输出)(等价于button2_Click)(相对于button5_Click更优雅的实现) private void button6_Click(object sender, EventArgs e) { Log("开始"); DateTime dt = DateTime.Now; //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //操作结果 object result1 = null; object result2 = null; object result3 = null; //方法调用 Await await = new Await(); await.Add(() => AsyncFunctionWithCallback(arg1, r => await.Collect(r, out result1))) .Add(() => AsyncFunctionWithCallback(arg2, r => await.Collect(r, out result2))) .Add(() => AsyncFunctionWithCallback(arg3, r => await.Collect(r, out result3))) .Completed(resultList => //输出结果 { Log(result1 != null ? result1.ToString() : "null"); Log(result2 != null ? result2.ToString() : "null"); Log(result3 != null ? result3.ToString() : "null"); Log("结束,耗时:" + DateTime.Now.Subtract(dt).TotalSeconds.ToString("0.000")); }) .Start(); } #endregion #region button7_Click 不使用状态机,执行带回调的异步方法(基本等价于button1_Click) private void button7_Click(object sender, EventArgs e) { Task.Run(() => { Log("开始"); DateTime dt = DateTime.Now; //输入参数 string arg1 = "1"; string arg2 = "2"; string arg3 = "3"; //操作结果 object result1 = null; object result2 = null; object result3 = null; //方法调用 AsyncFunctionWithCallback(arg1, r => result1 = r); AsyncFunctionWithCallback(arg2, r => result2 = r); AsyncFunctionWithCallback(arg3, r => result3 = r); //等待三个操作结果全部返回,三个操作结果返回之前,始终占用一个线程 while (result1 == null || result2 == null || result3 == null) { Thread.Sleep(1); } //输出结果 Log(result1 != null ? result1.ToString() : "null"); Log(result2 != null ? result2.ToString() : "null"); Log(result3 != null ? result3.ToString() : "null"); Log("结束,耗时:" + DateTime.Now.Subtract(dt).TotalSeconds.ToString("0.000")); }); } #endregion #region 同步方法 /// <summary> /// 同步方法 /// </summary> private string SyncFunction(string arg) { MemoryStream ms = HttpUtil.HttpDownloadFile(_testFileDownloadUrl); return "同步方法 arg=" + arg + " 下载文件的字节数组长度=" + ms.Length; } #endregion #region 异步方法 /// <summary> /// 异步方法 /// </summary> private async Task<string> AsyncFunction(string arg) { MemoryStream ms = await HttpUtil.HttpDownloadFileAsync(_testFileDownloadUrl); return "异步方法 arg=" + arg + " 下载文件的字节数组长度=" + ms.Length; } #endregion #region 带回调的异步方法 /// <summary> /// 带回调的异步方法 /// </summary> /// <param name="arg">传入参数</param> /// <param name="callback">回调</param> /// <param name="nextState">下一个状态</param> private void AsyncFunctionWithCallback(string arg, Action<object> callback) { HttpUtil.HttpDownloadFileAsyncWithCallback(_testFileDownloadUrl, new HttpUtilAsyncState() { State = 0 }, obj => { if (obj is Exception) { Exception ex = obj as Exception; Log("错误:" + ex.Message); } else { MemoryStream ms = obj as MemoryStream; string result = "带回调的异步方法 arg=" + arg + " 下载文件的字节数组长度=" + ms.Length; callback(result); } }); } #endregion #region 用Task模拟的异步方法 /* #region 异步方法 /// <summary> /// 异步方法 /// </summary> private Task<string> AsyncFunction(string arg) { return Task.Run<string>(() => { Thread.Sleep(2000); return "异步方法 arg=" + arg; }); } #endregion #region 带回调的异步方法 /// <summary> /// 带回调的异步方法 /// </summary> /// <param name="arg">传入参数</param> /// <param name="callback">回调</param> /// <param name="nextState">下一个状态</param> private void AsyncFunctionWithCallback(string arg, Action<object> callback) { Task.Run(() => { if (arg == "1") { Thread.Sleep(2000); } else { Thread.Sleep(2000); } string result = "带回调的异步方法 arg=" + arg; callback(result); }); } #endregion */ #endregion } }异步工具类Await.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Utils { /// <summary> /// 异步工具类 /// 代替async await语法糖 /// </summary> public class Await { /// <summary> /// 状态 /// </summary> private int _state { get; set; } /// <summary> /// 异步操作集合 /// </summary> private List<Action> _actionList = new List<Action>(); /// <summary> /// 操作结果集合 /// </summary> private List<object> _resultList = new List<object>(); /// <summary> /// 完成后的回调 /// </summary> private Action<List<object>> _completedCallback; /// <summary> /// 是否已调用回调函数 /// </summary> private bool _completedCalled = false; /// <summary> /// 锁 /// </summary> private object _lock = new object(); /// <summary> /// 异步工具类 构造函数 /// </summary> public Await() { _state = 0; } /// <summary> /// 添加一个异步操作 /// </summary> public Await Add(Action action) { _actionList.Add(action); return this; } /// <summary> /// 开始执行 /// </summary> public Await Start() { foreach (Action action in _actionList) { action(); } return this; } /// <summary> /// 收集结果 /// </summary> public Await Collect(object result, out object outResult) { outResult = result; _resultList.Add(result); lock (_lock) { _state++; if (_state == _actionList.Count && _completedCallback != null && !_completedCalled) { _completedCalled = true; _completedCallback(_resultList); } } return this; } /// <summary> /// 注册完成后的回调函数 /// </summary> public Await Completed(Action<List<object>> completedCallback) { this._completedCallback = completedCallback; return this; } } }文件下载工具类HttpUtil.cs下面的代码使用了三种方式实现文件下载同步方式实现文件下载异步方式实现文件下载通过回调和状态机实现的异步下载using Models; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Utils { /// <summary> /// Http上传下载文件 /// </summary> public class HttpUtil { #region HttpDownloadFile 下载文件 /// <summary> /// 下载文件 /// </summary> /// <param name="url">下载文件url路径</param> /// <param name="cookie">cookie</param> public static MemoryStream HttpDownloadFile(string url, CookieContainer cookie = null, WebHeaderCollection headers = null) { try { // 设置参数 HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; request.Method = "GET"; request.CookieContainer = cookie; if (headers != null) { foreach (string key in headers.Keys) { request.Headers.Add(key, headers[key]); } } //发送请求并获取相应回应数据 HttpWebResponse response = request.GetResponse() as HttpWebResponse; //直到request.GetResponse()程序才开始向目标网页发送Post请求 Stream responseStream = response.GetResponseStream(); //创建写入流 MemoryStream stream = new MemoryStream(); byte[] bArr = new byte[10240]; int size = responseStream.Read(bArr, 0, (int)bArr.Length); while (size > 0) { stream.Write(bArr, 0, size); size = responseStream.Read(bArr, 0, (int)bArr.Length); } stream.Seek(0, SeekOrigin.Begin); responseStream.Close(); return stream; } catch (Exception ex) { throw ex; } } #endregion #region HttpDownloadFile 下载文件(异步) /// <summary> /// 下载文件 /// </summary> /// <param name="url">下载文件url路径</param> /// <param name="cookie">cookie</param> public static async Task<MemoryStream> HttpDownloadFileAsync(string url, CookieContainer cookie = null, WebHeaderCollection headers = null) { try { // 设置参数 HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; request.Method = "GET"; request.CookieContainer = cookie; if (headers != null) { foreach (string key in headers.Keys) { request.Headers.Add(key, headers[key]); } } //发送请求并获取相应回应数据 HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse; //直到request.GetResponse()程序才开始向目标网页发送Post请求 Stream responseStream = response.GetResponseStream(); //创建写入流 MemoryStream stream = new MemoryStream(); byte[] bArr = new byte[10240]; int size = await responseStream.ReadAsync(bArr, 0, (int)bArr.Length); while (size > 0) { stream.Write(bArr, 0, size); size = await responseStream.ReadAsync(bArr, 0, (int)bArr.Length); } stream.Seek(0, SeekOrigin.Begin); responseStream.Close(); return stream; } catch (Exception ex) { throw ex; } } #endregion #region HttpDownloadFile 下载文件(基于回调的异步) /// <summary> /// 下载文件 /// </summary> /// <param name="url">下载文件url路径</param> /// <param name="cookie">cookie</param> public static void HttpDownloadFileAsyncWithCallback(string url, HttpUtilAsyncState state = null, Action<object> callback = null, CookieContainer cookie = null, WebHeaderCollection headers = null) { try { if (state.State == 0) { // 设置参数 HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; request.Method = "GET"; request.CookieContainer = cookie; if (headers != null) { foreach (string key in headers.Keys) { request.Headers.Add(key, headers[key]); } } //发送请求并获取相应回应数据 request.BeginGetResponse(asyncResult => { HttpUtilAsyncState asyncState = asyncResult.AsyncState as HttpUtilAsyncState; try { HttpWebResponse response = request.EndGetResponse(asyncResult) as HttpWebResponse; asyncState.ResponseStream = response.GetResponseStream(); HttpDownloadFileAsyncWithCallback(null, asyncState, callback); } catch (Exception ex) { asyncState.Exception = ex; asyncState.State = 2; HttpDownloadFileAsyncWithCallback(null, asyncState, callback); } }, new HttpUtilAsyncState { Request = request, State = 1 }); return; } if (state.State == 1) { byte[] bArr = new byte[10240]; state.ResponseStream.BeginRead(bArr, 0, (int)bArr.Length, asyncResult => { HttpUtilAsyncState asyncState = asyncResult.AsyncState as HttpUtilAsyncState; try { int size = state.ResponseStream.EndRead(asyncResult); if (size > 0) { state.Stream.Write(bArr, 0, size); HttpDownloadFileAsyncWithCallback(null, asyncState, callback); } else { asyncState.State = 2; HttpDownloadFileAsyncWithCallback(null, asyncState, callback); } } catch (Exception ex) { asyncState.Exception = ex; asyncState.State = 2; HttpDownloadFileAsyncWithCallback(null, asyncState, callback); } }, state); return; } if (state.State == 2) { if (state.Exception != null) { callback(state.Exception); return; } else { state.Stream.Seek(0, SeekOrigin.Begin); state.ResponseStream.Close(); callback(state.Stream); return; } } } catch (Exception ex) { throw ex; } } #endregion } } 源码https://gitee.com/s0611163/AsyncAwaitDemo原文地址https://www.cnblogs.com/s0611163/p/15835033.html
Java中super的用法super用于子类调用父类方法(private保护的方法除外)#子类默认会调用父类的无参构造器(在子类无参构造器的第一行中应为super())#1、注意如果父类定义了有参构造器那么子类便无法调用默认的无参构造器,解决方法应当给父类写出显式的无参构造器,或者子类调用父类构造器时添加参数super(参数);super的注意点:#1、super调用父类的构造方法,必须写在构造方法的第一行。2、super必须只能能出现在子类的方法或者构造方法中3、super和this不能同时调用构造方法。super与this的区别#1、代表的对象不同this:本身调用者这个对象super:代表父类对象的应用this:没有继承也能用super:只能在继承中使用构造方法的区别this():本类的构造super():父类的构造代码解析#//执行方法 public class application { public static void main(String[] args) { /*Demo09 student= new Demo09(); student.setName("dalao"); student.setAge(1000);//不合法数据 System.out.println(student.getName()+"年龄"+student.getAge());//println属于方法的重载 student.setAge(68);//合法数据 System.out.println(student.getAge());//println属于方法的重载 */ Demo11 student = new Demo11(); student.test01("小明"); student.test02(); } }//父类方法 public class Demo10 /*extends Object*/{ public Demo10(){ System.out.println("父类无参构造器已执行"); } protected String name="小明同学"; public void print(){//假如此处改为private私有其实也可继承只不过应当使用类如get,set方法来访问子类不能直接调用 System.out.println("我是父类"); } }//子类方法 public Demo11(){ super();//子类会默认调用父类的无参构造器 System.out.println("子类无参构造器已执行"); //super();而且调用父类无参构造器必须放在代码第一行 } private String name ="xiaomingtongxue"; public void test01(String name){ System.out.println(name); System.out.println(this.name); System.out.println(super.name); } public void print(){ System.out.println("我是子类"); } public void test02(){ print(); this.print(); super.print(); }结果#父类无参构造器已执行 子类无参构造器已执行 小明 xiaomingtongxue 小明同学 我是子类 我是子类 我是父类作者:Cn_FallTime出处:https://www.cnblogs.com/CnFallTime/p/15820397.html
Linux中Go环境配置和GoModuleGo环境配置和GoModuleLinux相关Linux常用操作mkdir directory ——创建文件夹 vi file ——创建文件,再关闭vim rm file ——删除文件 rm -rf directory ——递归删除文件夹,r代表递归,f带包强制 mv A B ——移动文件夹,可以用来改名 mv -rf ——同上 cp fromPath toPath ——拷贝,同样可以加-rf rename ——重命名,需要安装包 ls/ll/tree ——列出文件夹 cat ——打印文件内容 cd path ——进入路径 sudo apt install package ——Ubuntu安装vim常用操作:w ——写入 :q ——退出 :wq ——保存退出 home/end ——行首行位 G/gg ——文末,开头 pageup/pagedown ——翻页 backspace/delete ——前删,后删 dd/yy ——删除复制一样 ndd/nyy ——n换成数字,多行 p/P ——光标向下粘贴,光标向上粘贴 u/ctrl+u/ctrl+r ——撤销 :/word ——光标向下搜索 :?word ——光标向上搜索 :1,$s/word1/word2/g ——替换,1可省略 :1,$s/word1/word2/gc ——需要确认的替换 i ——进入输入模式 ESC ——退出输入模式 ctrl+q ——卡死强退输入模式 insert ——切换插入或替换输入golang环境配置1.官网下载go包体-拖进linux2.解压sudo tar -C /usr/local -zxvf go1.11.5.linux-amd64.tar.gz3.配置环境变量vim ~/.bashrc添加:#源码包 export GOROOT=/usr/local/go #用户工作路径 export GOPATH=$HOME/gopath #系统环境变量 export PATH=$PATH:$GOROOT/bin:$GOPATH/binsource ~/.bashrcgo verison——是否成功输出go版本;goModule任意位置创建项目文件夹;打开终端输入:go mod init + 包名文件夹下会生成go.mod 和 go.sum两个文件夹)go.mod记录了go的版本和依赖包的版本;go.sum是对依赖包整体去了hash记录,同时也多go.mod取哈希记录;保证多人合作时引用的包体版本一致;根据go.mod中依赖,通过go get下载同版本依赖包即可;Life is too short for so much sorrow.转载地址https://www.cnblogs.com/littleperilla/p/15820297.html
JAVA实现对阿里云DNS的解析管理1、阿里云DNS的SDK依赖 <dependency> <groupId>com.aliyun</groupId> <artifactId>tea-openapi</artifactId> <version>0.0.19</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>alidns20150109</artifactId> <version>2.0.1</version> </dependency> 2、第一个方法:创建SDK客户端实例所有解析记录的操作都要通过这个客户端实例来进行,所以要首先创建这个实例,需要阿里云的AccessKey(AppId和AppSecret) /** * <p> * 创建客户端实例 * </p> * * @return * @throws Exception */ private Client createClient() throws Exception{ AliConfig api = APIKit.getAliConfig(); //返回阿里云的AccessKey参数 if(api == null) throw new ErrException("未配置阿里云API参数!"); Config config = new Config(); config.accessKeyId = api.getAppId(); config.accessKeySecret = api.getAppSecret(); config.endpoint = "alidns.cn-beijing.aliyuncs.com"; return new Client(config); }3、第二个方法:返回指定的记录ID(RecordId)在阿里云的SDK中,对解析记录进行修改和删除时,都需要传入 RecordId 这个参数,所以提前写一个获取记录ID的方法。 /** * <p> * 返回指定主机记录的ID,不存在时返回null * </p> * * @param DomainName * @param RR 记录名称 * @return */ private String getRecId(Client client, String DomainName, String RR){ String recId = null; try { DescribeDomainRecordsRequest request = new DescribeDomainRecordsRequest(); request.setDomainName(DomainName); request.setRRKeyWord(RR); DescribeDomainRecordsResponse response = client.describeDomainRecords(request); if(response.getBody().getTotalCount() > 0){ List<DescribeDomainRecordsResponseBodyDomainRecordsRecord> recs = response.getBody().getDomainRecords().getRecord(); for(DescribeDomainRecordsResponseBodyDomainRecordsRecord rec: recs){ if(rec.getRR().equalsIgnoreCase(RR)){ recId = rec.getRecordId(); break; } } } } catch (Exception e) { } return recId; }4、第三个方法:添加或修改指定的记录方便起见,这里我将添加和修改集成到了一个方法,相当于SaveOrUpdate。 /** * <p> * 添加或修改解析记录 * </p> * * @param DomainName 域名 * @param RR 记录名称 * @param Type 记录类型(A、AAAA、MX、TXT、CNAME) * @param Value 记录值 */ public void update(String DomainName, String RR, String Type, String Value){ try { if(EStr.isEmpty(DomainName)) throw new RuntimeException("域名(DomainName)为空!"); if(EStr.isEmpty(RR)) throw new RuntimeException("主机记录(RR)为空!"); if(EStr.isEmpty(Type)) throw new RuntimeException("记录类型(Type)为空!"); if(EStr.isEmpty(Value)) throw new RuntimeException("记录值(Value)为空!"); Client client = createClient(); String recId = getRecId(client, DomainName, RR); if(EStr.isNull(recId)){ //添加 AddDomainRecordRequest request = new AddDomainRecordRequest(); request.setDomainName(DomainName); request.setRR(RR); request.setType(Type); request.setValue(Value); AddDomainRecordResponse response = client.addDomainRecord(request); recId = response.getBody().getRecordId(); }else{ //修改 UpdateDomainRecordRequest request = new UpdateDomainRecordRequest(); request.setRecordId(recId); request.setRR(RR); request.setType(Type); request.setValue(Value); UpdateDomainRecordResponse response = client.updateDomainRecord(request); recId = response.getBody().getRecordId(); } renderJson(Result.success("recId", recId)); } catch (Exception e) { renderJson(Result.fail(e.getMessage())); } }5、第四个方法:删除指定的记录这个很简单,根据查找到的RecordId直接删除即可。 /** * <p> * 删除记录 * </p> * * @param DomainName * @param RR */ public void remove(String DomainName, String RR){ try { if(EStr.isEmpty(DomainName)) throw new RuntimeException("域名(DomainName)为空!"); if(EStr.isEmpty(RR)) throw new RuntimeException("主机记录(RR)为空!"); Client client = createClient(); String recId = getRecId(client, DomainName, RR); if(EStr.isNull(recId)){ renderJson(Result.success("recId", null)); }else{ DeleteDomainRecordRequest request = new DeleteDomainRecordRequest(); request.setRecordId(recId); DeleteDomainRecordResponse response = client.deleteDomainRecord(request); renderJson(Result.success("recId", response.getBody().getRecordId())); } } catch (Exception e) { renderJson(Result.fail(e.getMessage())); } } 6、完整代码查看代码转载地址https://www.cnblogs.com/netWild/p/15815757.html
springCloudGateway-使用记录一、需求描述旧项目做好之后,已经维护了一两个月,基本上已经趋于稳定,按照项目的整体进度基本上不会在做什么改动。新项目已经确定下来,只是有一个大概的需求,unity3d的客户端已经开始做,在这个月23号之前会要求先出一个游戏的版本。目前相对来说不是太忙,自己就考虑着开始搭建新项目的框架,首先考虑的就是先搭建网关服务。之前的项目由于工期非常短,并且非常急就没有把网关服务给加进去,现在有时间了自己就想着第一个任务就是先添加网关服务,之后在根据需要在添加其他的服务。二、需求分析自己的一个了解,网关服务可以做很多事情,比如实现所有访问请求的日志采集;身份认证,是否已经登录,是否有权限登录某个系统;如果系统的访问量比较大的话,还可以做负载均衡;还可以做路由转发,根据不同的路由转发到对应的服务。好处这么多当然需要使用一个网关服务来作为项目的统一入口,做各种事情。三、技术选型由于现在技术更新得特别快,比如springboot的版本一直在不断地进行更新,支持时间也在不断地压缩。 参考了很多资料,最终自己采纳选择2.3.x的springboot版本,springcloud和springcloudalibaba的版本如下,<!-- 依赖的jar包所对应的版本 --><properties> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version> <!--<spring-cloud-starter-alibaba-nacos-config.version>1.4.2</spring-cloud-starter-alibaba-nacos-config.version>--> <!-- 编译插件 mvn compile --> <maven-compiler-plugin.source>1.8</maven-compiler-plugin.source> <maven-compiler-plugin.target>1.8</maven-compiler-plugin.target> <maven-compiler-plugin.encoding>UTF-8</maven-compiler-plugin.encoding> <!-- 是否跳过单元测试 解决中文乱码 --> <maven-surefire-plugin.skipTests>true</maven-surefire-plugin.skipTests> <maven-surefire-plugin.argLine>-Dfile.encoding=UTF-8</maven-surefire-plugin.argLine> <mybatis-generator-maven-plugin.version>1.3.2</mybatis-generator-maven-plugin.version></properties><!-- 单个子项目特有的jar包 --><dependencyManagement> <dependencies> <!-- springcloud版本 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- springcloud alibaba 版本 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>版本选定好之后就开始搭建项目。由于自己之前一直在使用springcloudalibaba的nacos组件,因此新搭建的项目中继续使用这个组件。主要用来做配置管理和服务发现。本篇博文需要有nacos使用基础的人才能理解。 首先搭建gateway网关项目,java代码很简单,就一个启动类,导入的maven配置如下:<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- lombok 仅限编译的时候使用 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> <!-- nacos 配置管理 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- nacos 服务发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency></dependencies>nacos中的配置如下:server.port=51309#路由ID 唯一即可,就类似于数据表中的主键IDspring.cloud.gateway.routes[0].id=motionBackend#路由转发的地址 lb表示 以load balance 负载均衡的 方式 将讲求转发到 motionSpacebackend(项目名称) 这个项目中,解析之后是一个具体的地址spring.cloud.gateway.routes[0].uri=lb://motionSpacebackend#访问地址中包含的路径spring.cloud.gateway.routes[0].predicates[0]=Path=/gateway/backend/**#StripPrefix 表示去掉前缀 1 表示第一个前缀 即使是去除 访问地址中的 /gateway/ 前缀spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1 bootstrap.properties的配置如下: spring.application.name=motionSpaceGatewayspring.profiles.active=dev#配置管理spring.cloud.nacos.config.server-addr=127.0.0.1:24700#spring.cloud.nacos.config.server-addr=192.168.1.2.5#命名空间spring.cloud.nacos.config.namespace=8760e76628114fceb7fc723b2da51c5c#分组配置spring.cloud.nacos.config.group=motionSpace# 开启动态刷新spring.cloud.nacos.config.refresh.enabled=true#读取的配置文件后缀名spring.cloud.nacos.config.file-extension=properties#服务发现地址spring.cloud.nacos.discovery.server-addr=127.0.0.1:24700#spring.cloud.nacos.discovery.server-addr=192.168.1.2.5 由于刚开始搭建项目,只要请求能够转发即可,之后的一些功能会一步一步的进行添加。做完这些工作后,启动项目,如下图 注意事项:springcloudgateway这个项目,不需要配置项目的根路径,如上图所示。它和其他springboot项目的启动方式还不太一样,其他项目可以配置项目的根路径,在项目启动时也会添加进去。可是springcloudgateway 即使在配置中添加了项目根路径,启动时也不会加载。上图显示的是Netty started on port,只需要配置一个端口即可。 之后是搭建一个后端主项目,导入的maven配置如下:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok 仅限编译的时候使用 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> <!-- nacos 配置管理 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- nacos 服务发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency></dependencies>配置更加简单,一个端口和一个访问路径即可:server.servlet.context-path=/backendserver.port=17303 bootstrap.properties的配置如下: spring.application.name=motionSpacebackendspring.profiles.active=dev#配置管理spring.cloud.nacos.config.server-addr=127.0.0.1:24700#spring.cloud.nacos.config.server-addr=192.168.1.2.5#命名空间spring.cloud.nacos.config.namespace=8760e76628114fceb7fc723b2da51c5c#分组配置spring.cloud.nacos.config.group=motionSpace# 开启动态刷新spring.cloud.nacos.config.refresh.enabled=true#读取的配置文件后缀名spring.cloud.nacos.config.file-extension=properties#服务发现地址spring.cloud.nacos.discovery.server-addr=127.0.0.1:24700#spring.cloud.nacos.discovery.server-addr=192.168.1.2.5然后添加一个测试类@Slf4j@RestController@RequestMapping("/test")public class TestController { /** * @description: * @author: dengyilang * @date: 2022/1/7 17:40 * @return: void */ @RequestMapping(value = "/rout", method = RequestMethod.GET) public String scanBind(){ log.info("backend路由测试---"); return "路由测试"; }}项目启动成功。 首先测试后端项目能否正常访问,访问的地址为 http://localhost:17303/backend/test/rout 测试结果正常。 然后测试使用网关服务进行转发是否正常,请求地址为 http://localhost:51309/gateway/backend/test/rout 测试结果正确。请求的大致过程为 首先请求地址 http://localhost:51309/gateway/backend/test/rout 会到达 gateway项目的后台,根据路由匹配规则,匹配到如下图中的路由,并且会截取掉第一个前缀/gateway/变为 http://localhost:51309/backend/test/rout 然后gateway项目将请求进行转发到下图的地址中,gateway项目在底层进行处理时,会将motionSpacebackend这个项目名称解析为对应的地址和端口,在本地就解析为127.0.0.1:17303 转发后的请求就变为http://127.0.0.1:17303/backend/test/rout 最终正确地返回想要的结果。到此一个简单的网关服务就搭建完成,之后在使用过程中遇到任何问题都会更新在本篇博客中。参考博客https://www.cnblogs.com/hellohero55/p/12723451.html转载地址https://www.cnblogs.com/yilangcode/p/15777129.html
Java I/O模型及其底层原理 Java I/O是Java基础之一,在面试中也比较常见,在这里我们尝试通过这篇文章阐述Java I/O的基础概念,帮助大家更好的理解Java I/O。在刚开始学习Java I/O时,我很迷惑,因为网上绝大多数的文章都是讲解Linux网络I/O模型的,那时我总是搞不明白和Java I/O的关系。后来查了看了好多,才明白Java I/O的原理是以Linux网络I/O模型为基础的,理解了Linux网络I/O模型再学习Java I/O就很方便了,所以这篇文章,我们先来了解I/O的基本概念,再学习Linux网络I/O模型,最后再看Java中的几种I/O。 什么是I/O?I/O是Input、Output的缩写,即对应计算机中的输入输出,以一次文件读取为例,我们需要将磁盘上的数据读取到用户空间,那么这次数据转移操作其实就是一次I/O操作,更具体的说是一次文件I/O。我们浏览网页,其中在请求一个网页时,服务器通过网络把数据发送给我们,此时程序将数据从TCP缓冲区复制到用户空间,那么这次数据转移操作其实也是一次I/O操作,更具体的说是一次网络I/O。I/O到处都在,十分重要,Java对I/O对底层操作系统的各种I/O模型进行了封装,使我们可以轻松开发。 Linux网络I/O模型根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型,分别是:阻塞I/O(Blocking I/O)、非阻塞I/O(Non-Blacking I/O)、I/O多路复用模型(I/O Multiplexing)、信号驱动式I/O(Signal Driven I/O)、异步I/O(Asynchronous I/O)。我们逐步了解一下其基本原理。 阻塞I/O(Blocking I/O)阻塞I/O是最早最基础的I/O模型,其在读写数据过程中会阻塞。通过下图我们可以看到,当用户进程调用了recvfrom这个系统调用后,内核开始第一阶段的数据准备工作,直到内核等待数据准备完成,然后开始第二阶段的将数据从内核复制到用户空间的工作,最后内核返回结果。整个过程中用户进程都是阻塞的,直到最后返回结果后才接触阻塞block状态。阻塞I/O模型适用于并发量小且对时延不敏感的系统。 非阻塞I/O(Non-Blacking I/O)当用户进程调用recvfrom这个系统调用后,如果内核尚未准备好数据,此时不再阻塞用户进程,而是立即返回一个EWOULDBLOCK错误。用户进程会不断发起系统调用直到内核中数据被准备好(轮询),此时将执行第二阶段的将数据从内核复制到用户空间的工作,然后内核返回结果。非阻塞I/O模型不断地轮询往往需要耗费大量cpu时间。 I/O多路复用模型(I/O Multiplexing)I/O多路复用的优点在于单个进程可以同时处理多个网络连接的I/O,其基本原理就是select/epoll函数可以不断的轮询其负责的所有socket,当某个socket有数据到达时,就通知用户进程。如下图所示,当用户进程调用select函数时,整个进程会被阻塞block住,但是这里的阻塞不是被socket I/O阻塞,而是被select这个函数阻塞。同时内核会监听改select负责的所有socket(这里的socket一般设置为non-blocking),当任何一个socket中的数据准备好时,select就会返回给用户进程,这时候用户进程再此发起一个系统调用,将数据从内核复制到用户空间,并返回结果。对比I/O多路复用模型和阻塞I/O模型的流程,多路复用多了一个系统调用来完成select环节,除此之外没有太大的不同。Select的优势在于它可以同时处理多个connection,但是会多一个系统调用。多路复用本质上也不是非阻塞的。 信号驱动式I/O(Signal Driven I/O)首先我们开启socket的信号驱动I/O功能,然后用户进程发起sigaction系统调用给内核后立即返回并可继续处理其他工作。收到sigaction系统调用的内核在将数据准备好后会按照要求产生一个signo信号通知给用户进程。然后用户进程再发起recvfrom系统调用,完成数据从内核到用户空间的复制,并返回最终结果。其基础原理图示如下: 异步I/O(Asynchronous I/O)用户进程向内核发起系统调用后,就可以开始去做其他事情了。内核收到异步I/O的系统调用后,会直接retrun,所以这里不会对用户进程有阻塞。之后内核等待数据准备完成后会继续将数据从内核拷贝到用户空间(具体动作可以由异步I/O调用定义),然后内核回给用户进程发送一个signal,告诉用户进程I/O操作完成了,整个过程不会导致用户请求进程阻塞。信号驱动I/O模型是内核通知我们可以发起I/O操作了,而异步I/O模式是内核告诉我们I/O操作已经完成了。 以上就是Linux的5种网络I/O模型,其中前4中都是同步I/O模型,他们真正的I/O操作环节都会将进程阻塞,只有最后一种异步I/O模型是异步I/O操作。 Java中的I/O模型在JDK1.4之前,基于Java的所有socket通信都是使用阻塞I/O(BIO),JDK1.4提供了了非阻塞I/O(NIO)功能,不过虽然名字叫做NIO,实际底层模型是I/O多路复用,JDK1.7提供了针对异步I/O(AIO)功能。 BIOBIO简化了上层开发,但是性能瓶颈问题严重,对高并发第时延支持差。基于消息队列和线程池技术优化的BIO模式虽然可以对高并发支持有一定帮助,但是还是受限于线程池大小和线程池阻塞队列大小的制约,当并发数超过线程池的处理能力时,部分请求法务继续处理,会导致客户端连接超时,影响用户体验。 NIONIO弥补了BIO的不足,简单说就是通过selector不断轮询注册在自己上面的channel,如果channel上面有新的连接读写时间时就会被轮询出来,一个selector上面可以注册多个channel,一个线程就可以负责selector的轮询,这样就可以支持成千上万的连接。Selector就是一个轮询器,channel是一个通道,通过它来读取或者写入数据,通道是双向的,可以用于读、写、读和写。Buffer用来和channel交互,数据通过channel进出buffer。NIO的优点是可以可靠性好以及高并发低时延,但是使用NIO的代码开发较为复杂。 AIOAIO,或者说叫做NIO2.0,引入了异步channel的概念,提供了异步文件channel和异步socket channel的实现,开发者可以通过Future类来表示异步操作的结果,也可以在执行异步操作时传入一个channels,实现CompletionHandler接口作为回调。AIO不用开发者单独开发独立线程的selector,异步回调操作有JDK地城思安城池负责驱动,开发起来比NIO简单一些,同时保持了高可靠高并发低时延的优点。 参考:https://blog.csdn.net/historyasamirror/article/details/5778378https://juejin.im/post/5cce5019e51d453a506b0ebf 原文地址https://www.cnblogs.com/guodongdidi/p/13085474.html
Linux的文件系统及文件缓存知识点整理 Linux的文件系统#文件系统的特点#文件系统要有严格的组织形式,使得文件能够以块为单位进行存储。文件系统中也要有索引区,用来方便查找一个文件分成的多个块都存放在了什么位置。如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层。文件应该用文件夹的形式组织起来,方便管理和查询。Linux内核要在自己的内存里面维护一套数据结构,来保存哪些文件被哪些进程打开和使用。总体来说,文件系统的主要功能梳理如下: ext系列的文件系统的格式#inode与块的存储#硬盘分成相同大小的单元,我们称为块(Block)。一块的大小是扇区大小的整数倍,默认是4K。在格式化的时候,这个值是可以设定的。 一大块硬盘被分成了一个个小的块,用来存放文件的数据部分。这样一来,如果我们像存放一个文件,就不用给他分配一块连续的空间了。我们可以分散成一个个小块进行存放。这样就灵活得多,也比较容易添加、删除和插入数据。 inode就是文件索引的意思,我们每个文件都会对应一个inode;一个文件夹就是一个文件,也对应一个inode。 inode数据结构如下: Copystruct ext4_inode { __le16 i_mode; /* File mode */ __le16 i_uid; /* Low 16 bits of Owner Uid */ __le32 i_size_lo; /* Size in bytes */ __le32 i_atime; /* Access time */ __le32 i_ctime; /* Inode Change time */ __le32 i_mtime; /* Modification time */ __le32 i_dtime; /* Deletion Time */ __le16 i_gid; /* Low 16 bits of Group Id */ __le16 i_links_count; /* Links count */ __le32 i_blocks_lo; /* Blocks count */ __le32 i_flags; /* File flags */ ...... __le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */ __le32 i_generation; /* File version (for NFS) */ __le32 i_file_acl_lo; /* File ACL */ __le32 i_size_high; ......};inode里面有文件的读写权限i_mode,属于哪个用户i_uid,哪个组i_gid,大小是多少i_size_io,占用多少个块i_blocks_io,i_atime是access time,是最近一次访问文件的时间;i_ctime是change time,是最近一次更改inode的时间;i_mtime是modify time,是最近一次更改文件的时间等。 所有的文件都是保存在i_block里面。具体保存规则由EXT4_N_BLOCKS决定,EXT4_N_BLOCKS有如下的定义: Copy define EXT4_NDIR_BLOCKS 12 define EXT4_IND_BLOCK EXT4_NDIR_BLOCKS define EXT4_DIND_BLOCK (EXT4_IND_BLOCK + 1) define EXT4_TIND_BLOCK (EXT4_DIND_BLOCK + 1) define EXT4_N_BLOCKS (EXT4_TIND_BLOCK + 1) 在ext2和ext3中,其中前12项直接保存了块的位置,也就是说,我们可以通过i_block[0-11],直接得到保存文件内容的块。 但是,如果一个文件比较大,12块放不下。当我们用到i_block[12]的时候,就不能直接放数据块的位置了,要不然i_block很快就会用完了。 那么可以让i_block[12]指向一个块,这个块里面不放数据块,而是放数据块的位置,这个块我们称为间接块。如果文件再大一些,i_block[13]会指向一个块,我们可以用二次间接块。二次间接块里面存放了间接块的位置,间接块里面存放了数据块的位置,数据块里面存放的是真正的数据。如果文件再大点,那么i_block[14]同理。 这里面有一个非常显著的问题,对于大文件来讲,我们要多次读取硬盘才能找到相应的块,这样访问速度就会比较慢。 为了解决这个问题,ext4做了一定的改变。它引入了一个新的概念,叫作Extents。比方说,一个文件大小为128M,如果使用4k大小的块进行存储,需要32k个块。如果按照ext2或者ext3那样散着放,数量太大了。但是Extents可以用于存放连续的块,也就是说,我们可以把128M放在一个Extents里面。这样的话,对大文件的读写性能提高了,文件碎片也减少了。 Exents是一个树状结构: 每个节点都有一个头,ext4_extent_header可以用来描述某个节点。 Copystruct ext4_extent_header { __le16 eh_magic; /* probably will support different formats */ __le16 eh_entries; /* number of valid entries */ __le16 eh_max; /* capacity of store in entries */ __le16 eh_depth; /* has tree real underlying blocks? */ __le32 eh_generation; /* generation of the tree */ };eh_entries表示这个节点里面有多少项。这里的项分两种,如果是叶子节点,这一项会直接指向硬盘上的连续块的地址,我们称为数据节点ext4_extent;如果是分支节点,这一项会指向下一层的分支节点或者叶子节点,我们称为索引节点ext4_extent_idx。这两种类型的项的大小都是12个byte。 Copy/* This is the extent on-disk structure. It's used at the bottom of the tree.*/ struct ext4_extent { __le32 ee_block; /* first logical block extent covers */ __le16 ee_len; /* number of blocks covered by extent */ __le16 ee_start_hi; /* high 16 bits of physical block */ __le32 ee_start_lo; /* low 32 bits of physical block */ };/* This is index on-disk structure. It's used at all the levels except the bottom.*/ struct ext4_extent_idx { __le32 ei_block; /* index covers logical blocks from 'block' */ __le32 ei_leaf_lo; /* pointer to the physical block of the next * * level. leaf or next index could be there */ __le16 ei_leaf_hi; /* high 16 bits of physical block */ __u16 ei_unused; };如果文件不大,inode里面的i_block中,可以放得下一个ext4_extent_header和4项ext4_extent。所以这个时候,eh_depth为0,也即inode里面的就是叶子节点,树高度为0。 如果文件比较大,4个extent放不下,就要分裂成为一棵树,eh_depth>0的节点就是索引节点,其中根节点深度最大,在inode中。最底层eh_depth=0的是叶子节点。 除了根节点,其他的节点都保存在一个块4k里面,4k扣除ext4_extent_header的12个byte,剩下的能够放340项,每个extent最大能表示128MB的数据,340个extent会使你的表示的文件达到42.5GB。 inode位图和块位图#inode的位图大小为4k,每一位对应一个inode。如果是1,表示这个inode已经被用了;如果是0,则表示没被用。block的位图同理。 在Linux操作系统里面,想要创建一个新文件,会调用open函数,并且参数会有O_CREAT。这表示当文件找不到的时候,我们就需要创建一个。那么open函数的调用过程大致是:要打开一个文件,先要根据路径找到文件夹。如果发现文件夹下面没有这个文件,同时又设置了O_CREAT,就说明我们要在这个文件夹下面创建一个文件。 创建一个文件,那么就需要创建一个inode,那么就会从文件系统里面读取inode位图,然后找到下一个为0的inode,就是空闲的inode。对于block位图,在写入文件的时候,也会有这个过程。 文件系统的格式#数据块的位图是放在一个块里面的,共4k。每位表示一个数据块,共可以表示$4 * 1024 * 8 = 2{15}$个数据块。如果每个数据块也是按默认的4K,最大可以表示空间为$2{15} * 4 * 1024 = 2^{27}$个byte,也就是128M,那么显然是不够的。 这个时候就需要用到块组,数据结构为ext4_group_desc,这里面对于一个块组里的inode位图bg_inode_bitmap_lo、块位图bg_block_bitmap_lo、inode列表bg_inode_table_lo,都有相应的成员变量。 这样一个个块组,就基本构成了我们整个文件系统的结构。因为块组有多个,块组描述符也同样组成一个列表,我们把这些称为块组描述符表。 我们还需要有一个数据结构,对整个文件系统的情况进行描述,这个就是超级块ext4_super_block。里面有整个文件系统一共有多少inode,s_inodes_count;一共有多少块,s_blocks_count_lo,每个块组有多少inode,s_inodes_per_group,每个块组有多少块,s_blocks_per_group等。这些都是这类的全局信息。 最终,整个文件系统格式就是下面这个样子。 默认情况下,超级块和块组描述符表都有副本保存在每一个块组里面。防止这些数据丢失了,导致整个文件系统都打不开了。 由于如果每个块组里面都保存一份完整的块组描述符表,一方面很浪费空间;另一个方面,由于一个块组最大128M,而块组描述符表里面有多少项,这就限制了有多少个块组,128M * 块组的总数目是整个文件系统的大小,就被限制住了。 因此引入Meta Block Groups特性。 首先,块组描述符表不会保存所有块组的描述符了,而是将块组分成多个组,我们称为元块组(Meta Block Group)。每个元块组里面的块组描述符表仅仅包括自己的,一个元块组包含64个块组,这样一个元块组中的块组描述符表最多64项。 我们假设一共有256个块组,原来是一个整的块组描述符表,里面有256项,要备份就全备份,现在分成4个元块组,每个元块组里面的块组描述符表就只有64项了,这就小多了,而且四个元块组自己备份自己的。 根据图中,每一个元块组包含64个块组,块组描述符表也是64项,备份三份,在元块组的第一个,第二个和最后一个块组的开始处。 如果开启了sparse_super特性,超级块和块组描述符表的副本只会保存在块组索引为0、3、5、7的整数幂里。所以上图的超级块只在索引为0、3、5、7等的整数幂里。 目录的存储格式#其实目录本身也是个文件,也有inode。inode里面也是指向一些块。和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。这些信息我们称为ext4_dir_entry。 在目录文件的块中,最简单的保存格式是列表,每一项都会保存这个目录的下一级的文件的文件名和对应的inode,通过这个inode,就能找到真正的文件。第一项是“.”,表示当前目录,第二项是“…”,表示上一级目录,接下来就是一项一项的文件名和inode。 如果在inode中设置EXT4_INDEX_FL标志,那么就表示根据索引查找文件。索引项会维护一个文件名的哈希值和数据块的一个映射关系。 如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。然后打开这个块,如果里面不再是索引,而是索引树的叶子节点的话,那里面还是ext4_dir_entry的列表,我们只要一项一项找文件名就行。通过索引树,我们可以将一个目录下面的N多的文件分散到很多的块里面,可以很快地进行查找。 Linux中的文件缓存#ext4文件系统层#对于ext4文件系统来讲,内核定义了一个ext4_file_operations。 Copyconst struct file_operations ext4_file_operations = {...... .read_iter = ext4_file_read_iter, .write_iter = ext4_file_write_iter, ......}ext4_file_read_iter会调用generic_file_read_iter,ext4_file_write_iter会调用__generic_file_write_iter。 Copyssize_tgeneric_file_read_iter(struct kiocb iocb, struct iov_iter iter){...... if (iocb->ki_flags & IOCB_DIRECT) { ...... struct address_space *mapping = file->f_mapping; ...... retval = mapping->a_ops->direct_IO(iocb, iter); } ...... retval = generic_file_buffered_read(iocb, iter, retval); } ssize_t __generic_file_write_iter(struct kiocb iocb, struct iov_iter from){...... if (iocb->ki_flags & IOCB_DIRECT) { ...... written = generic_file_direct_write(iocb, from); ...... } else { ...... written = generic_perform_write(file, from, iocb->ki_pos); ...... } }generic_file_read_iter和__generic_file_write_iter有相似的逻辑,就是要区分是否用缓存。因此,根据是否使用内存做缓存,我们可以把文件的I/O操作分为两种类型。 第一种类型是缓存I/O。大多数文件系统的默认I/O操作都是缓存I/O。对于读操作来讲,操作系统会先检查,内核的缓冲区有没有需要的数据。如果已经缓存了,那就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。对于写操作来讲,操作系统会先将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说,写操作就已经完成。至于什么时候再写到磁盘中由操作系统决定,除非显式地调用了sync同步命令。 第二种类型是直接IO,就是应用程序直接访问磁盘数据,而不经过内核缓冲区,从而减少了在内核缓存和用户程序之间数据复制。 如果在写的逻辑__generic_file_write_iter里面,发现设置了IOCB_DIRECT,则调用generic_file_direct_write,里面同样会调用address_space的direct_IO的函数,将数据直接写入硬盘。 带缓存的写入操作#我们先来看带缓存写入的函数generic_perform_write。 Copyssize_t generic_perform_write(struct file *file, struct iov_iter *i, loff_t pos) { struct address_space *mapping = file->f_mapping; const struct address_space_operations *a_ops = mapping->a_ops; do { struct page *page; unsigned long offset; /* Offset into pagecache page */ unsigned long bytes; /* Bytes to write to page */ status = a_ops->write_begin(file, mapping, pos, bytes, flags, &page, &fsdata); copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes); flush_dcache_page(page); status = a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata); pos += copied; written += copied; balance_dirty_pages_ratelimited(mapping); } while (iov_iter_count(i)); }循环中主要做了这几件事: 对于每一页,先调用address_space的write_begin做一些准备;调用iov_iter_copy_from_user_atomic,将写入的内容从用户态拷贝到内核态的页中;调用address_space的write_end完成写操作;调用balance_dirty_pages_ratelimited,看脏页是否太多,需要写回硬盘。所谓脏页,就是写入到缓存,但是还没有写入到硬盘的页面。对于第一步,调用的是ext4_write_begin来说,主要做两件事: 第一做日志相关的工作。 ext4是一种日志文件系统,是为了防止突然断电的时候的数据丢失,引入了日志(Journal)模式。日志文件系统比非日志文件系统多了一个Journal区域。文件在ext4中分两部分存储,一部分是文件的元数据,另一部分是数据。元数据和数据的操作日志Journal也是分开管理的。你可以在挂载ext4的时候,选择Journal模式。这种模式在将数据写入文件系统前,必须等待元数据和数据的日志已经落盘才能发挥作用。这样性能比较差,但是最安全。 另一种模式是order模式。这个模式不记录数据的日志,只记录元数据的日志,但是在写元数据的日志前,必须先确保数据已经落盘。这个折中,是默认模式。 还有一种模式是writeback,不记录数据的日志,仅记录元数据的日志,并且不保证数据比元数据先落盘。这个性能最好,但是最不安全。 第二调用grab_cache_page_write_begin来,得到应该写入的缓存页。 Copystruct page grab_cache_page_write_begin(struct address_space mapping, pgoff_t index, unsigned flags) { struct page *page; int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT; page = pagecache_get_page(mapping, index, fgp_flags, mapping_gfp_mask(mapping)); if (page) wait_for_stable_page(page); return page; }在内核中,缓存以页为单位放在内存里面,每一个打开的文件都有一个struct file结构,每个struct file结构都有一个struct address_space用于关联文件和内存,就是在这个结构里面,有一棵树,用于保存所有与这个文件相关的的缓存页。 对于第二步,调用iov_iter_copy_from_user_atomic。先将分配好的页面调用kmap_atomic映射到内核里面的一个虚拟地址,然后将用户态的数据拷贝到内核态的页面的虚拟地址中,调用kunmap_atomic把内核里面的映射删除。 Copysize_t iov_iter_copy_from_user_atomic(struct page *page, struct iov_iter *i, unsigned long offset, size_t bytes) { char *kaddr = kmap_atomic(page), *p = kaddr + offset; iterate_all_kinds(i, bytes, v, copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len), memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page, v.bv_offset, v.bv_len), memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len) ) kunmap_atomic(kaddr); return bytes; }第三步中,调用ext4_write_end完成写入。这里面会调用ext4_journal_stop完成日志的写入,会调用block_write_end->__block_commit_write->mark_buffer_dirty,将修改过的缓存标记为脏页。可以看出,其实所谓的完成写入,并没有真正写入硬盘,仅仅是写入缓存后,标记为脏页。 第四步,调用 balance_dirty_pages_ratelimited,是回写脏页。 Copy/** balance_dirty_pages_ratelimited - balance dirty memory state @mapping: address_space which was dirtied* Processes which are dirtying memory should call in here once for each page which was newly dirtied. The function will periodically check the system's dirty state and will initiate writeback if needed. */ void balance_dirty_pages_ratelimited(struct address_space *mapping){ struct inode *inode = mapping->host; struct backing_dev_info *bdi = inode_to_bdi(inode); struct bdi_writeback *wb = NULL; int ratelimit; ...... if (unlikely(current->nr_dirtied >= ratelimit)) balance_dirty_pages(mapping, wb, current->nr_dirtied); ......}在balance_dirty_pages_ratelimited里面,发现脏页的数目超过了规定的数目,就调用balance_dirty_pages->wb_start_background_writeback,启动一个背后线程开始回写。 另外还有几种场景也会触发回写: 用户主动调用sync,将缓存刷到硬盘上去,最终会调用wakeup_flusher_threads,同步脏页;当内存十分紧张,以至于无法分配页面的时候,会调用free_more_memory,最终会调用wakeup_flusher_threads,释放脏页;脏页已经更新了较长时间,时间上超过了设定时间,需要及时回写,保持内存和磁盘上数据一致性。带缓存的读操作#看带缓存的读,对应的是函数generic_file_buffered_read。 Copystatic ssize_t generic_file_buffered_read(struct kiocb *iocb, struct iov_iter *iter, ssize_t written) { struct file *filp = iocb->ki_filp; struct address_space *mapping = filp->f_mapping; struct inode *inode = mapping->host; for (;;) { struct page *page; pgoff_t end_index; loff_t isize; page = find_get_page(mapping, index); if (!page) { if (iocb->ki_flags & IOCB_NOWAIT) goto would_block; page_cache_sync_readahead(mapping, ra, filp, index, last_index - index); page = find_get_page(mapping, index); if (unlikely(page == NULL)) goto no_cached_page; } if (PageReadahead(page)) { page_cache_async_readahead(mapping, ra, filp, page, index, last_index - index); } /* * Ok, we have the page, and it's up-to-date, so * now we can copy it to user space... */ ret = copy_page_to_iter(page, offset, nr, iter); } }在generic_file_buffered_read函数中,我们需要先找到page cache里面是否有缓存页。如果没有找到,不但读取这一页,还要进行预读,这需要在page_cache_sync_readahead函数中实现。预读完了以后,再试一把查找缓存页。 如果第一次找缓存页就找到了,我们还是要判断,是不是应该继续预读;如果需要,就调用page_cache_async_readahead发起一个异步预读。 最后,copy_page_to_iter会将内容从内核缓存页拷贝到用户内存空间。 作者: luozhiyun 出处:https://www.cnblogs.com/luozhiyun/p/13061199.html
如何监控 Linux 服务器状态? Linux 服务器我们天天打交道,特别是 Linux 工程师更是如此。为了保证服务器的安全与性能,我们经常需要监控服务器的一些状态,以保证工作能顺利开展。 本文介绍的几个命令,不仅仅适用于服务器监控,也适用于我们日常情况下的开发。 watch 命令我们的使用频率很高,它的基本作用是,按照指定频率重复执行某一条指令。使用这个命令,我们可以重复调用一些命令来达到监控服务器的作用。 默认情况下,watch 命令的执行周期是 2 秒,但我们可以使用 -n 选项来指定运行频率,比如我们想要每隔 5 秒执行 date 命令,可以这么执行: $ watch -n 5 date一台服务器肯定有多人在用,特别是本部门的小伙伴。对于这些小伙伴有没浑水摸鱼,我们可以使用一些命令来监控他们。 我们可以每隔 10 秒执行 who 命令,来看看都有谁在使用服务器。 $ watch -n 10 whoEvery 10.0s: who butterfly: Tue Jan 23 16:02:03 2019 shs :0 2019-01-23 09:45 (:0)dory pts/0 2019-01-23 15:50 (192.168.0.5)alvin pts/1 2019-01-23 16:01 (192.168.0.15)shark pts/3 2019-01-23 11:11 (192.168.0.27)如果发现系统运行很慢,我们可以调用 uptime 命令来查看系统平均负载情况。 $ watch uptimeEvery 2.0s: uptime butterfly: Tue Jan 23 16:25:48 2019 16:25:48 up 22 days, 4:38, 3 users, load average: 1.15, 0.89, 1.02一些关键的进程肯定不能挂,否则可能会影响到业务开展,所以我们可以重复统计服务器中的所有进程数量。 $ watch -n 5 'ps -ef | wc -l'Every 5.0s: ps -ef | wc -l butterfly: Tue Jan 23 16:11:54 2019 245想动态知道服务器内存使用情况,可以重复执行 free 命令。 $ watch -n 5 free -mEvery 5.0s: free -m butterfly: Tue Jan 23 16:34:09 2019 total used free shared buff/cache available Mem: 5959 776 3276 12 1906 4878Swap: 2047 0 2047当然不仅仅是这些,我们还可以重复调用很多命令来对服务器一些关键参数进行监控, top使用 top 命令我们可以知道系统的很多关键参数,而且是动态更新的。默认情况下,top 监控的是系统的整体状态,如果我们只想知道某个人的使用情况,可以使用 -u 选项来指定这个人。 $ top -u alvintop - 16:14:33 up 2 days, 4:27, 3 users, load average: 0.00, 0.01, 0.02Tasks: 199 total, 1 running, 198 sleeping, 0 stopped, 0 zombie%Cpu(s): 0.0 us, 0.2 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 stMiB Mem : 5959.4 total, 3277.3 free, 776.4 used, 1905.8 buff/cacheMiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 4878.4 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND23026 alvin 20 0 46340 7820 6504 S 0.0 0.1 0:00.05 systemd23033 alvin 20 0 149660 3140 72 S 0.0 0.1 0:00.00 (sd-pam)23125 alvin 20 0 63396 5100 4092 S 0.0 0.1 0:00.00 sshd23128 alvin 20 0 16836 5636 4284 S 0.0 0.1 0:00.03 zsh在这个结果里,你不仅仅可以看到 alvin 这个用户运行的所有的进程数,也可以看到每个进程所消耗的系统资源(CPU,内存),同时依然可以看到整个系统的关键参数。 ac如果你想知道每个用户登录服务器所使用的时间,你可以使用 ac 命令。这个命令需要你安装 acct 包(Debian)或 psacct 包(RHEL,Centos)。 如果我们想知道所有用户登陆服务器所使用的时间之和,我们可以直接运行 ac 命令,无需任何参数。 $ ac total 1261.72 如果我们想知道各个用户所使用时间,可以加上 -p 选项。 $ ac -p shark 5.24 alvin 5.52 shs 1251.00 total 1261.76 我们还可以通过加上 -d 选项来查看具体每一天用户使用服务器时间之和。 $ ac -d | tail -10Jan 11 total 0.05Jan 12 total 1.36Jan 13 total 16.39Jan 15 total 55.33Jan 16 total 38.02Jan 17 total 28.51Jan 19 total 48.66Jan 20 total 1.37Jan 22 total 23.48Today total 9.83小结我们可以使用很多命令来监控系统的运行状态,本文主要介绍了三个:watch 命令可以让你重复执行某一条命令来监控一些参数的变化,top 命令可以查看某个用户运行的进程数以及消耗的资源,而 ac 命令则可以查看每个用户使用服务器时间。你经常使用哪个命令呢?欢迎留言讨论! 原文地址https://www.cnblogs.com/yychuyu/p/13056012.html
【Spring注解开发】组件注册-使用@Configuration和@Bean给容器中注册组件 写在前面在之前的Spring版本中,我们只能通过写XML配置文件来定义我们的Bean,XML配置不仅繁琐,而且很容易出错,稍有不慎就会导致编写的应用程序各种报错,排查半天,发现是XML文件配置不对!另外,每个项目编写大量的XML文件来配置Spring,也大大增加了项目维护的复杂度,往往很多个项目的Spring XML文件的配置大部分是相同的,只有很少量的配置不同,这也造成了配置文件上的冗余。 项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation Spring IOC和DI在Spring容器的底层,最重要的功能就是IOC和DI,也就是控制反转和依赖注入。 IOC:控制反转,将类的对象的创建交给Spring类管理创建。DI:依赖注入,将类里面的属性在创建类的过程中给属性赋值。DI和IOC的关系:DI不能单独存在,DI需要在IOC的基础上来完成。 在Spring内部,所有的组件都会放到IOC容器中,组件之间的关系通过IOC容器来自动装配,也就是我们所说的依赖注入。接下来,我们就使用注解的方式来完成容器组件的注册、管理及依赖、注入等功能。 在介绍使用注解完成容器组件的注册、管理及依赖、注入等功能之前,我们先来看看使用XML文件是如何注入Bean的。 通过XML文件注入JavaBean首先,我们在工程的io.mykit.spring.bean包下创建Person类,作为测试的JavaBean,代码如下所示。 package io.mykit.spring.bean; import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;import java.io.Serializable; /** @author binghe @version 1.0.0 @description 测试实体类*/ @Data@ToString@NoArgsConstructor@AllArgsConstructorpublic class Person implements Serializable { private static final long serialVersionUID = 7387479910468805194L; private String name; private Integer age; }接下来,我们在工程的resources目录下创建Spring的配置文件beans.xml,通过beans.xml文件将Person类注入到Spring的IOC容器中,配置如下所示。 <?xml version="1.0" encoding="UTF-8"?> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id = "person" class="io.mykit.spring.bean.Person"> <property name="name" value="binghe"></property> <property name="age" value="18"></property> </bean> 到此,我们使用XML方式注入JavaBean就配置完成了。接下来,我们创建一个SpringBeanTest类来进行测试,这里,我使用的是Junit进行测试,测试方法如下所示。@Testpublic void testXmlConfig(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Person person = (Person) context.getBean("person"); System.out.println(person); }运行testXmlConfig()方法,输出的结果信息如下。 Person(name=binghe, age=18)从输出结果中,我们可以看出,Person类通过beans.xml文件的配置,已经注入到Spring的IOC容器中了。 通过注解注入JavaBean通过XML文件,我们可以将JavaBean注入到Spring的IOC容器中。那使用注解又该如何实现呢?别急,其实使用注解比使用XML文件要简单的多,我们在项目的io.mykit.spring.plugins.register.config包下创建PersonConfig类,并在PersonConfig类上添加@Configuration注解来标注PersonConfig类是一个Spring的配置类,通过@Bean注解将Person类注入到Spring的IOC容器中。 package io.mykit.spring.plugins.register.config; import io.mykit.spring.bean.Person;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration; /** @author binghe @version 1.0.0 @description 以注解的形式来配置Person*/ @Configurationpublic class PersonConfig { @Bean public Person person(){ return new Person("binghe001", 18); } }没错,通过PersonConfig类我们就能够将Person类注入到Spring的IOC容器中,是不是很Nice!!主要我们在类上加上@Configuration注解,并在方法上加上@Bean注解,就能够将方法中创建的JavaBean注入到Spring的IOC容器中。 接下来,我们在SpringBeanTest类中创建一个testAnnotationConfig()方法来测试通过注解注入的Person类,如下所示。 @Testpublic void testAnnotationConfig(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class); Person person = context.getBean(Person.class); System.out.println(person); }运行testAnnotationConfig()方法,输出的结果信息如下所示。 Person(name=binghe001, age=18)可以看出,通过注解将Person类注入到了Spring的IOC容器中。 到这里,我们已经明确,通过XML文件和注解两种方式都可以将JavaBean注入到Spring的IOC容器中。那么,使用注解将JavaBean注入到IOC容器中时,使用的bean的名称是什么呢? 我们可以在testAnnotationConfig()方法中添加如下代码来获取Person类型下的注解名称。 //按照类型找到对应的bean名称数组String[] names = context.getBeanNamesForType(Person.class);Arrays.stream(names).forEach(System.out::println);完整的testAnnotationConfig()方法的代码如下所示。 @Testpublic void testAnnotationConfig(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class); Person person = context.getBean(Person.class); System.out.println(person); //按照类型找到对应的bean名称数组 String[] names = context.getBeanNamesForType(Person.class); Arrays.stream(names).forEach(System.out::println); }运行testAnnotationConfig()方法输出的结果信息如下所示。 Person(name=binghe001, age=18)person那这里的person是啥?我们修改下PersonConfig类中的person()方法,将person()方法修改成person01()方法,如下所示。 package io.mykit.spring.plugins.register.config; import io.mykit.spring.bean.Person;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration; /** @author binghe @version 1.0.0 @description 以注解的形式来配置Person*/ @Configurationpublic class PersonConfig { @Bean public Person person01(){ return new Person("binghe001", 18); } }此时,我们再次运行testAnnotationConfig()方法,输出的结果信息如下所示。 Person(name=binghe001, age=18)person01看到这里,大家应该有种豁然开朗的感觉了,没错!!使用注解注入Javabean时,bean在IOC中的名称就是使用@Bean注解标注的方法名称。我们可不可以为bean单独指定名称呢?那必须可以啊!只要在@Bean注解中明确指定名称就可以了。比如下面的PersonConfig类的代码,我们将person01()方法上的@Bean注解修改成@Bean("person")注解,如下所示。 package io.mykit.spring.plugins.register.config; import io.mykit.spring.bean.Person;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration; /** @author binghe @version 1.0.0 @description 以注解的形式来配置Person*/ @Configurationpublic class PersonConfig { @Bean("person") public Person person01(){ return new Person("binghe001", 18); } }此时,我们再次运行testAnnotationConfig()方法,输出的结果信息如下所示。 Person(name=binghe001, age=18)person可以看到,此时,输出的JavaBean的名称为person。 结论:我们在使用注解方式向Spring的IOC容器中注入JavaBean时,如果没有在@Bean注解中明确指定bean的名称,就使用当前方法的名称来作为bean的名称;如果在@Bean注解中明确指定了bean的名称,则使用@Bean注解中指定的名称来作为bean的名称。 好了,咱们今天就聊到这儿吧!别忘了给个在看和转发,让更多的人看到,一起学习一起进步!! 项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation 原文地址https://www.cnblogs.com/binghe001/p/13052276.html
【Spring注解驱动开发】聊聊Spring注解驱动开发那些事儿! 写在前面今天,面了一个工作5年的小伙伴,面试结果不理想啊!也不是我说,工作5年了,问多线程的知识:就只知道继承Thread类和实现Runnable接口!问Java集合,竟然说HashMap是线程安全的!问MySQL的MyISAM存储引擎和InnoDB存储引擎的区别,竟然说成是MyISAM存储引擎支持事务,InnoDB不支持!问Spring就只知道IOC和AOP的概念,深一点就不知道了!再问项目。。。哎,算了,不说了! 大家对于设计模式、高并发和Java8新特性,不了解的,就去看我的专栏吧!今天,我们来聊聊关于Spring注解驱动开发的那些事儿,也算是Spring专栏的开篇吧! 关于Spring说起Spring,绝对是Java开发领域的佼佼者,试问,做Java开发的有谁不知道Spring?做Java开发的又有谁没用过Spring?又有哪家公司在Java Web项目中没使用过Spring?就算有,那也应该很少吧!所以,骚年,如果你选择了Java开发这条不归路,你就必须牢牢掌握Spring! Spring注解驱动如果小伙伴们还在用Spring的基础框架,例如:Spring、SpringMVC、MyBatis,也就是传说中的SSM,来整合开发的时候,可能会大量的写配置文件。那么,在SpringBoot和SpringCloud兴起之后,Spring的注解驱动就用的非常多了!其中,会用到非常多的注解。为了能够更加深刻的理解这些注解的原理,更好的使用这些注解提高我们的工作效率。这里,我结合实际工作中使用Spring的一些经验,向大家分享下如何使用Spring的注解来提高我们的工作效率,以及注解背后的工作原理到底是什么! 专栏安排不知道怎么安排这个专栏,反正想了很久,无意间看到一张脑图,哈哈,没错,可以按照它来(文末会给出这张图)。那我就把整个专栏分成三个大的部分吧,分别是:容器、扩展原理和Web。 容器容器作为整个专栏的第一大部分,内容包括: AnnotationConfigApplicationContext组件添加组件赋值组件注入AOP声明式事务扩展原理扩展原理作为整个专栏的第二大部分,内容包括: BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessorApplicationListenerSpring容器创建过程在这部分,我们一起来研究Spring的底层源码和运行流程,对于很多小伙伴来说,这部分的内容相当枯燥,甚至有种身体被掏空的感觉(哈哈),但是,这部分的内容一定要掌握,这也是普通程序员进阶成为高级程序员的必经之路。 这部分内容对于深度学习Spring框架,起着非常重要的作用。小伙伴们在看这部分的文章时,一定要根据文章自己多动手调试Spring源码,这样对于Spring的理解才能更加深刻。 WebWeb作为整个专栏的第三大部分,内容包括: servlet3.0异步请求这部分,其实就是SpringMVC,这个部分中,我们会重点来说异步请求。 整个专栏的规划有一定深度,建议小伙伴们提前学习了解下Spring的基础知识,最好是对Spring和SpringMVC框架有过一定的使用经验,如果是事先了解过Spring和SpringMVC的源码,那就太好了,这样学习起来可以达到事半功倍的效果。 暂时就说这么多吧,今天算是开篇了,小伙伴们有啥想说的,都可以私聊我! 写在最后如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习Spring注解驱动开发。公众号回复“spring注解”关键字,领取Spring注解驱动开发核心知识图,让Spring注解驱动开发不再迷茫。 最后,附上Spring注解驱动开发核心知识图,祝大家在学习Spring注解驱动开发时少走弯路。 原文地址https://www.cnblogs.com/binghe001/p/13047333.html
面试问题---JAVA程序CPU占用过高怎么定位 今天一个电话面试问了这个问题。回来查了下答案,自己也顺带操作一遍,做个记录。之前只知道jstack工具可以查看线程状态这些。比如死锁这些,主要是之前不知道top -H -p pid这个命令的使用,这命令可以看到进程下面线程信息,拿到线程ID,然后再结合jstack命令使用就可以解决这个问题了。下面记录一下具体的操作步骤: 1.打个jar包丢到机器上运行 package com.nijunyang.test; public class TestApplication { public static void main(String[] args) { for (int i = 0; i < 50; i++) { new Thread(()->test()).start(); } } public static void test() { while (true) { int a = 1 + 6; System.out.println(a); } } } 使用这个maven插件 打包jar <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>com.nijunyang.test.TestApplication</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> java -jar test-0.0.1-SNAPSHOT-jar-with-dependencies.jar 运行程序 一直在输出 3.top |grep java 或者 jps指令找到java进程的pid(6167) 4. top -H -p pid 以线程的形式查看该进程 top -H -p 6167 因为我们程序是起了50个线程 所以这里就会展示这个进程中的所有线程呢 5.前面的线程ID是10进制的,,需要转换成16进制,,因为等下在jstack命令取出来的线程ID是16进制的:这里就随便选一个线程ID 去转换了,真实环境肯定是选择CPU占用率最高的那个线程,echo "obase=16;6219" | bc 6.jstack 6167 >threadInfo.txt 信息输出到文件 然后查看。也可以直接在命令里面查看 7.文件中查找184b的线程ID信息,就可以找到是哪个线程导致的内存占用过高,同时也能看到具体的代码位置 原文地址https://www.cnblogs.com/nijunyang/p/13040511.html
学Linux驱动: 应该先了解驱动模型 [导读] Linux设备林林总总,嵌入式开发一个绕不开的话题就是设备驱动开发,在做具体设备驱动开发之前,有必要对Linux设驱动模型有一个相对清晰的认识,将会帮助驱动开发,明白具体驱动接口操作符相应都做些什么。 个人对于驱动模型的理解概括起来就是一句话:利用面向对象编程思想,实现设备分层管理软件体系结构。 注:代码分析基于linux-5.4.31 为啥要驱动模型随着系统结构演化越来越复杂,Linux内核对设备描述衍生出一般性的抽象描述,形成一个分层体系结构,从而引入了设备驱动模型。这样描述还是不够让人理解,来看一下这些需求就好理解些: Linux内核可以在各种体系结构和硬件平台上运行,因此需要最大限度地提高代码在平台之间的可重用性。分层实现也实现了软件工程的高内聚-低耦合的设计思想。低耦合体现在对外提供统一的抽象访问接口,高内聚将相关度紧密的集中抽象实现。Linux内核驱动程序模型是先前在内核中使用的所有不同驱动程序模型的统一。 它旨在通过将一组数据和操作整合到全局可访问的数据结构中,来扩展基于基础总线来桥接设备驱动程序。传统的驱动模型为它们所控制的设备实现了某种类似于树的结构(有时只是一个列表)。不同类型的总线之间没有任何一致性。 驱动模型抽象了啥当前驱动程序模型为描述总线和总线下可能出现的设备提供了一个通用的、统一的模型。统一总线模型包括一组所有总线都具有的公共属性和一组公共回调,如总线探测期间的设备发现、总线关闭、总线电源管理等。 通用的设备和桥接接口反映了现代计算机的目标:即执行无缝设备“即插即用”,电源管理和热插拔的能力。 特别是,英特尔和微软规定的模型(即ACPI)可确保与x86兼容的系统上几乎任何总线上的几乎所有设备都可以在此范式下工作。 当然,虽然大多数总线都支持其中大多数操作,但并不是每条总线都能够支持所有此类操作。 那么哪些通用需求被抽象出来了呢? 电源系统和系统关机,对于电源管理与系统关机对于设备相关的操作进行抽象实现。关机为什么要被抽象出来管理,比如设备操作正在进行此时系统收到关机指令,那么在设备模型层就会遍历系统设备硬件,确保系统正确关机。用户空间访问:sysfs虚拟文件系统实现与设备模型对外的访问抽象,这也是为什么说Linux 设备也是文件的由来。实际从软件架构层面看,这其实是一个软件桥接模块,抽象出统一用户访问接口,桥接了设备驱动。热插拔管理:热插拔管理机制定义统一的抽象接口操作符kset_hotplug_ops,不同设备利用操作符实现差异化。设备类型:设备分类机制,从高层级抽象描述设备类型,具体可以在sysfs下面体现。用户空间访问由于具有系统中所有设备的完整分层视图,因此将完整的分层视图导出到用户空间变得相对容易。 这是通过实现名为sysfs虚拟文件系统来完成的。 sysfs的自动挂载通常是通过/etc/fstab文件中的以下条目来完成的: none /sys sysfs defaults 0 0对于Debian系统而言,可能在/lib/init/fstab采用下面的形式挂载: none /sys sysfs nodev,noexec,nosuid 0 0当然也可以采用手动方式挂载: mount -t sysfs sysfs /sys 当将设备插入树中时,都会为其创建一个目录。该目录可以填充在发现的每个层(全局层,总线层或设备层)中。 全局层当前创建两个文件-'name'和'power'。 前者报告设备名称。 后者报告设备的当前电源状态。 它还将用于设置当前电源状态。 总线层为探测总线时发现的设备创建文件。 例如,PCI层当前为每个PCI设备创建“ irq”和“resource”文件。 特定于设备的驱动程序也可以在其目录中导出文件,以暴露特定于设备的数据或可用接口。 驱动模型实现先来梳理一下内部几个主要与驱动模型相关的数据结构: ./include/linux/Device.h 定义设备驱动主要数据结构 bus_type:抽象描述总线类型,如USB/PCI/I2C/MMC等device_driver:实现具体连接在总线上的设备驱动。device:描述连接在总线上的设备./include/linux/Kobject.h中定义了隐藏在后台的类似于基类的数据结构: kset:可以认为是kobject的顶层容器类。每个kset内部都包含了自己的kobject.kobject:在 sysfs 中出现的每个对象都对应一个 kobject, 它和内核交互来创建它的可见表述,每一个 kobject 对应 文件系统 /sys 里的一个 目录,目录的名字就是结构体中的 name bus_typebus_type用以驱动总线,具体的驱动USB/I2C/PCI/MMC等: 注册总线,利用bus_register注册总线,bus_unregister删除总线。如下例子,每种总线须定义一个bus_type对象,并利用bus_register注册总线,或bus_unregister删除总线。/i2c-core-base.c/struct bus_type i2c_bus_type = { .name = "i2c", .match = i2c_device_match, .probe = i2c_device_probe, .remove = i2c_device_remove, .shutdown = i2c_device_shutdown, };EXPORT_SYMBOL_GPL(i2c_bus_type);static int __init i2c_init(void){ int retval; retval = of_alias_get_highest_id("i2c"); down_write(&__i2c_board_lock); if (retval >= __i2c_first_dynamic_bus_num) __i2c_first_dynamic_bus_num = retval + 1; up_write(&__i2c_board_lock); /*注册I2C总线*/ retval = bus_register(&i2c_bus_type); if (retval) return retval; is_registered = true; ifdef CONFIG_I2C_COMPAT i2c_adapter_compat_class = class_compat_register("i2c-adapter"); if (!i2c_adapter_compat_class) { retval = -ENOMEM; goto bus_err; } endif retval = i2c_add_driver(&dummy_driver); if (retval) goto class_err; if (IS_ENABLED(CONFIG_OF_DYNAMIC)) WARN_ON(of_reconfig_notifier_register(&i2c_of_notifier)); if (IS_ENABLED(CONFIG_ACPI)) WARN_ON(acpi_reconfig_notifier_register(&i2c_acpi_notifier)); return 0; class_err: ifdef CONFIG_I2C_COMPAT class_compat_unregister(i2c_adapter_compat_class); bus_err: endif is_registered = false; /*错误时删除总线*/ bus_unregister(&i2c_bus_type); return retval; }注册适配器驱动程序(USB控制器,I2C适配器等),以检测连接的设备,并提供与设备的通信机制图中的match函数接口用于将驱动程序与设备进行匹配。match回调的目的是使总线有机会通过比较驱动程序支持的设备ID与特定设备的设备ID来确定特定驱动程序是否支持特定设备,而不会牺牲特定于总线的功能或类型安全性 。当向总线注册驱动程序时,将遍历总线的设备列表,并为每个没有与之关联的驱动程序的设备调用match回调。提供API函数以实现适配器驱动以及设备驱动。同时dev_pm_ops *pm实现对于总线的功耗管理接口抽象。对于特定总线实现这个操作符对应的函数。struct dev_pm_ops { int (*prepare)(struct device *dev); void (*complete)(struct device *dev); int (*suspend)(struct device *dev); int (*resume)(struct device *dev); int (*freeze)(struct device *dev); int (*thaw)(struct device *dev); int (*poweroff)(struct device *dev); int (*restore)(struct device *dev); int (*suspend_late)(struct device *dev); int (*resume_early)(struct device *dev); int (*freeze_late)(struct device *dev); int (*thaw_early)(struct device *dev); int (*poweroff_late)(struct device *dev); int (*restore_early)(struct device *dev); int (*suspend_noirq)(struct device *dev); int (*resume_noirq)(struct device *dev); int (*freeze_noirq)(struct device *dev); int (*thaw_noirq)(struct device *dev); int (*poweroff_noirq)(struct device *dev); int (*restore_noirq)(struct device *dev); int (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); int (*runtime_idle)(struct device *dev); };iommu_ops 操作符提供总线相关的IOMMU抽象。设备驱动注册到总线上时,将在sysfs管理总线/设备/设备驱动的层次关系,以PCI为例:/在总线上注册的驱动程序会在总线的驱动程序目录中获得一个目录//sys/bus/pci/ |-- devices `-- drivers |-- Intel ICH |-- Intel ICH Joystick |-- agpgart `-- e100 /在该类型的总线上发现的每个设备都会在总线的设备目录中获得到物理层次结构中该设备目录的符号链接//sys/bus/pci/ |-- devices | |-- 00:00.0 -> ../../../root/pci0/00:00.0 | |-- 00:01.0 -> ../../../root/pci0/00:01.0 | `-- 00:02.0 -> ../../../root/pci0/00:02.0 `-- drivers 总线属性:bus_groups/设备属性dev_groups/驱动属性drv_groups。 device作用:抽象描述具体的设备设备注册:发现设备的总线驱动程序使用下面的函数来向内核注册设备int device_register(struct device * dev);利用device_unregister()从总线上删除设备device_driver作用:抽象描述连接在总线上的具体设备的驱动驱动注册,通过下面的函数将设备驱动程序注册int driver_register(struct device_driver *drv);使用它使用以下命令从驱动程序目录中添加和删除属性 int driver_create_file(struct device_driver , const struct driver_attribute ); void driver_remove_file(struct device_driver , const struct driver_attribute );class作用:抽象设备的高层视图,描述的是设备的集合。抽象了同类型的设备的底层实现细节。比如所有的网络接口都位于/sys/class/net下struct subsys_private *p描述类链表kobject/ksetkobject类似于面向对象中的内核基类,内核利用它将各个对象连接起来组成分层的机构体系,其parent指针将形成一个树状分层结构。kset内部包含了kobject。重心在描述对象的聚集于集合。这也是set一词的含义。每一个kset添加到系统中,都将在sysfs中创建一个目录kobject/kset一起实现了sysfs虚拟文件系统中设备/总线/设备驱动树状分层结构的最关键的底层实现由来。总体上而言:通过上面一些关键数据结构关系分析,总线设备驱动模型最终目的是实现如下这样一个分层驱动模型。 文章出自微信公众号:嵌入式客栈 原文地址https://www.cnblogs.com/embInn/p/13034226.html
ASP.NET Core Blazor Webassembly 之 数据绑定 上一次我们学习了Blazor组件相关的知识(Asp.net Core Blazor Webassembly - 组件)。这次继续学习Blazor的数据绑定相关的知识。当代前端框架都离不开数据绑定技术。数据绑定技术以数据为主导来驱动UI界面,用户对数据的修改会实时提现在UI上,极大的提高了开发效率,让开发者从繁琐的dom操作中解脱出来。对于数据绑定.NET开发者并不会陌生,WPF里大量应用数据绑定技术,有过WPF开发经验的同学其实很容易理解前端的数据绑定。总之数据绑定技术及其概念、思维极其重要。下面让我们看看Blazor的数据绑定技术。 单向绑定Blazor的数据绑定官方文档是直接从双向绑定开始的,但我觉得有必要说一下单向绑定。因为其他框架一般都会区分单向、双向,比如vue的v-bind单向,v-model就是双向。我们这里分开讲也有利于跟其他框架进行对比。下面我们实现一个计数器组件来演示下单向数据绑定。 使用@进行绑定@page "/counter" Current count: @currentCount Click me @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } } 这个Counter组件默认的项目就自带。跟我们使用服务端Razor一样,使用@符号在需要替换值的地方插入对应的变量。这个值就会被渲染在相应的地方。当我们在前端修改变量的时候,对应的ui界面会同步进行修改。 使用@bind-{attribute}进行绑定除了直接使用@进行绑定,我们还可以使用@bind-{attribute}来实现对html元素属性的绑定,比如对style,class内容进行绑定。下面演示下对class进行绑定。我们把p元素的class绑定到“currentClass”字段。 @page "/counter" Counter current count: @currentCount Click me @code { private string currentClass = "text-danger"; private int currentCount = 0; private void IncrementCount() { currentCount++; } } 使用@bind-{attribute}进行绑定有个比较奇怪的问题,当你使用@bind-{attribute}进行绑定的时候必须同时指定@bind-{attribute}:event。@bind-{attribute}:event是用来指定双向绑定的时候控件在发生某个事件的时候回写值到绑定的字段上。可是p,div这种元素根本不可能会激发onchange,oninput这种事件,也不可能去修改绑定的字段的值,这个用法感觉有点多此一举。Blazor的单向数据绑定的用法跟ASP.NET Core MVC的Razor基本相似,不同点就是Blazor不需要Http回发到服务器就可以实时渲染新的界面出来。 双向绑定双向绑定主要使用在一些输入控件上,比如input,select等。当我们对这些控件上的值进行修改后会回写绑定的字段。这种特性在表单场景中非常有用。我们定义一个用户信息编辑的组件来演示下: @page "/infoedit" userName: @userName sex: @sex userName: <input @bind="userName" /> sex: <select @bind="sex"> <option value="m">男</option> <option value="f">女</option> </select> @code { private string userName="abc"; private string sex="f"; } 当我们运行这个组件,在文本框进行修改后,鼠标点击其他地方让文本框失去焦点值就会回写到绑定的字段上,上面的单向绑定信息会自动同步。但是如果你用过VUE或者Angularjs的双向绑定就会觉得失去焦点再回写字段数据太慢了,一点也不酷。要知道VUE的双向绑定可是实时同步的,那么Blazor如何做到在输入的同时就更新值呢,答案是使用@bind:event来指定回写的激发事件,我们改成“oninput”事件就可以实现: userName: <input @bind="userName" @bind:event="oninput"/> 双向绑定的多种写法看到这里也许你也明白了,@bind真正的本质是由对value的绑定和对某个事件的绑定协同完成的。这点跟VUE非常相似。@bind其实是@bind-value的缩写,我们可以用@bind-value来实现双向绑定: userName: <input @bind-value="userName" @bind-value:event="oninput"/> 以上写法的效果跟@bind一模一样。再进一步,@bind-value也只是对@的包装,我们可以使用@来实现双向绑定:@page "/infoedit" userName: @userName sex: @sex userName: <input value="@userName" @oninput="oninput"/> sex: <select @bind="sex"> <option value="m">男</option> <option value="f">女</option> </select> @code { private string userName="abc"; private string sex="f"; private void oninput(ChangeEventArgs e) { userName = e.Value.ToString(); } }以上代码的效果跟@bind一模一样。通过使用@对value直接进行绑定以及绑定一个oninput事件进行值的回写,同样实现了双向绑定。 格式化时间字符串使用@bind:format 可以对绑定时间类型字段的时候进行格式化: 出生日期: 这个功能有点类似Angularjs的filter功能,但是目前只能对时间进行格式化,功能很弱。 父组件绑定数据到子组件组件之间往往都是嵌套的,很多子组件都依赖父组件的数据来决定如何呈现,这种场景非常常见。我们还是继续修改上面的编辑组件,用户信息不在自己初始化,而是从父组件传递过来:子组件: ====================child================== userName: <input @bind="UserInfo.UserName" /> sex: <select @bind="UserInfo.Sex"> <option value="m">男</option> <option value="f">女</option> </select> BrithDay:<input @bind="UserInfo.BrithDay" /> @code {[Parameter] public UserInfo UserInfo { get; set; } [Parameter] public EventCallback<UserInfo> UserInfoChanged { get; set; } }子组件定义一个UserInfo对象并且使用[Parameter]进行标记,同时如果父组件使用@bind-UserInfo来绑定的话,还必须实现一个UserInfoChanged事件。父组件: @page "/"====================parent================== userName: @userInfo.UserName sex: @userInfo.Sex brithday: @userInfo.BrithDay @code { private UserInfo userInfo; protected override void OnInitialized() { userInfo = new UserInfo { UserName = "abc", Sex = "f", BrithDay = DateTime.Now }; base.OnInitialized(); } } 父组件初始化一个UserInfo对象后通过@bind-UserInfo绑定给子组件。注意这里我们修改子组件的值并不会同步给父组件,所以可以看到@bind-UserInfo的传值还是单向的。 子组件传值给父组件 ??原来我以为父组件使用@bind-UserInfo并且子组件实现了对应的changed方法就可以实现子组件跟父组件的自动传值,就跟input的双向绑定一样。但是不管我怎么试都没有卵用。如果只是单向的那为什么要这么大费周章?我直接使用属性赋值不就可以了么?像下面这样: 直接通过组件的属性直接把父组件的数据传递到子组件,效果跟上面是一样的,而且这样子组件我还能少写一个changed事件。我原本以为使用基本类型,比如string可以自动双向绑定,然后并没有什么卵用。没有办法我继续尝试父组件监听UserInfoChanged事件来接受子组件的数据,然后VS提示我同一个事件不能绑定两次。 我已经无语了,难道要我再定义一个事件吗?于是我放弃了@bind-来实现子组件给父组件传值,我直接使用属性赋值难道不比这个简单吗?子组件修改数据的时候不断对外抛事件: ====================child================== userName: <input @bind="UserInfo.UserName" @oninput="InvokeChanged"/> sex: <select @bind="UserInfo.Sex"> <option value="m">男</option> <option value="f">女</option> </select> BrithDay:<input @bind="UserInfo.BrithDay" /> @code {[Parameter] public UserInfo UserInfo { get; set; } [Parameter] public EventCallback<UserInfo> UserInfoChanged { get; set; } private void InvokeChanged() { UserInfoChanged.InvokeAsync(this.UserInfo); Console.WriteLine("InvokeChanged"); } }父组件监听事件后更新数据: @page "/"====================parent`================== userName: @userInfo.UserName sex: @userInfo.Sex brithday: @userInfo.BrithDay title: @title @code { private UserInfo userInfo; private string title; protected override void OnInitialized() { userInfo = new UserInfo { UserName = "abc", Sex = "f", BrithDay = DateTime.Now }; base.OnInitialized(); } private void HandleUserInfoChanged(UserInfo info) { this.userInfo.UserName = info.UserName; Console.WriteLine("HandleUserInfoChanged"); } } 我原以为这样就没什么问题了,可奇怪的是,父组件页面重新渲染需要在子组件第二次修改数据后呈现且呈现的是前一次的。 到这里我已经无语了,最后我只能在子组件直接添加一个按钮,修改完后点击保存来触发InvokeChanged事件,这样子是可以的: ====================child================== userName: <input @bind="UserInfo.UserName" /> sex: <select @bind="UserInfo.Sex"> <option value="m">男</option> <option value="f">女</option> </select> BrithDay:<input @bind="UserInfo.BrithDay" /> 保存 @code { [Parameter] public UserInfo UserInfo { get; set; } [Parameter] public EventCallback<UserInfo> UserInfoChanged { get; set; } private void InvokeChanged() { UserInfoChanged.InvokeAsync(this.UserInfo); Console.WriteLine("InvokeChanged"); } } 到此数据绑定也演示完了,可是关于子组件往父组件传值的事我实在没像明白,难道是我哪里错了? 最后附上代码:BlazorWasmDataBind 作者:Agile.Zhou(kklldog) 原文地址https://www.cnblogs.com/kklldog/p/blazor-wasm-databind.html
一篇有趣的负载均衡算法实现负载平衡(Load balancing)是一种在多个计算机(网络、CPU、磁盘)之间均匀分配资源,以提高资源利用的技术。使用负载均衡可以最大化服务吞吐量,可能最小化响应时间,同时由于使用负载均衡时,会使用多个服务器节点代单点服务,也提高了服务的可用性。 负载均衡的实现可以软件可以硬件,硬件如大名鼎鼎的 F5 负载均衡设备,软件如 NGINX 中的负载均衡实现,又如 Springcloud Ribbon 组件中的负载均衡实现。 如果看到这里你还不知道负载均衡是干嘛的,那么只能放一张图了,毕竟没图说个啥。 负载均衡要做到在多次请求下,每台服务器被请求的次数大致相同。但是实际生产中,可能每台机器的性能不同,我们会希望性能好的机器承担的请求更多一些,这也是正常需求。 如果这样说下来你看不懂,那我就再举个例子好了,一排可爱的小熊(服务器)站好。 这时有人(用户)要过来打脸(请求访问)。 那么怎么样我们才能让这每一个可爱的小熊被打的次数大致相同呢? 又或者熊 4 比较胖,抗击打能力是别人的两倍,我们怎么提高熊 4 被打的次数也是别人的两倍呢? 又或者每次出手的力度不同,有重有轻,恰巧熊 4 总是承受这种大力度啪啪打脸,熊 4 即将不省熊事,还要继续打它吗? 这些都是值的思考的问题。 说了那么多,口干舌燥,我双手已经饥渴难耐了,迫不及待的想要撸起代码了。 随机访问上面说了,为了负载均衡,我们必须保证多次出手后,熊 1 到熊 4 被打次数均衡。比如使用随机访问法,根据数学上的概率论,随机出手次数越多,每只熊被打的次数就会越相近。代码实现也比较简单,使用一个随机数,随机访问一个就可以了。 /* 服务器列表 /private static List serverList = new ArrayList<>();static { serverList.add("192.168.1.2"); serverList.add("192.168.1.3"); serverList.add("192.168.1.4"); serverList.add("192.168.1.5"); } /** 随机路由算法*/ public static String random() { // 复制遍历用的集合,防止操作中集合有变更 List<String> tempList = new ArrayList<>(serverList.size()); tempList.addAll(serverList); // 随机数随机访问 int randomInt = new Random().nextInt(tempList.size()); return tempList.get(randomInt); }因为使用了非线程安全的集合,所以在访问操作时操作的是集合的拷贝,下面几种轮训方式中也是这种思想。 写一个模拟请求方法,请求10w次,记录请求结果。 public static void main(String[] args) { HashMap<String, Integer> serverMap = new HashMap<>(); for (int i = 0; i < 20000; i++) { String server = random(); Integer count = serverMap.get(server); if (count == null) { count = 1; } else { count++; } // 记录 serverMap.put(server, count); } // 路由总体结果 for (Map.Entry<String, Integer> entry : serverMap.entrySet()) { System.out.println("IP:" + entry.getKey() + ",次数:" + entry.getValue()); } }运行得到请求结果。 IP:192.168.1.3,次数:24979IP:192.168.1.2,次数:24896IP:192.168.1.5,次数:25043IP:192.168.1.4,次数:25082每台服务器被访问的次数都趋近于 2.5w,有点负载均衡的意思。但是随机毕竟是随机,是不能保证访问次数绝对均匀的。 轮训访问轮训访问就简单多了,拿上面的熊1到熊4来说,我们一个接一个的啪啪 - 打脸,熊1打完打熊2,熊2打完打熊3,熊4打完打熊1,最终也是实现了被打均衡。但是保证均匀总是要付出代价的,随机访问中需要随机,轮训访问中需要什么来保证轮训呢? /* 服务器列表 /private static List serverList = new ArrayList<>();static { serverList.add("192.168.1.2"); serverList.add("192.168.1.3"); serverList.add("192.168.1.4"); serverList.add("192.168.1.5"); }private static Integer index = 0; /** 随机路由算法*/ public static String randomOneByOne() { // 复制遍历用的集合,防止操作中集合有变更 List<String> tempList = new ArrayList<>(serverList.size()); tempList.addAll(serverList); String server = ""; synchronized (index) { index++; if (index == tempList.size()) { index = 0; } server = tempList.get(index);; } return server; }由代码里可以看出来,为了保证轮训,必须记录上次访问的位置,为了让在并发情况下不出现问题,还必须在使用位置记录时进行加锁,很明显这种互斥锁增加了性能开销。 依旧使用上面的测试代码测试10w次请求负载情况。 IP:192.168.1.3,次数:25000IP:192.168.1.2,次数:25000IP:192.168.1.5,次数:25000IP:192.168.1.4,次数:25000 轮训加权上面演示了轮训方式,还记的一开始提出的熊4比较胖抗击打能力强,可以承受别人2倍的挨打次数嘛?上面两种方式都没有体现出来熊 4 的这个特点,熊 4 窃喜,不痛不痒。但是熊 1 到 熊 3 已经在崩溃的边缘,不行,我们必须要让胖着多打,能者多劳,提高整体性能。 /* 服务器列表 /private static HashMap serverMap = new HashMap<>();static { serverMap.put("192.168.1.2", 2); serverMap.put("192.168.1.3", 2); serverMap.put("192.168.1.4", 2); serverMap.put("192.168.1.5", 4); }private static Integer index = 0; /** 加权路由算法*/ public static String oneByOneWithWeight() { List<String> tempList = new ArrayList(); HashMap<String, Integer> tempMap = new HashMap<>(); tempMap.putAll(serverMap); for (String key : serverMap.keySet()) { for (int i = 0; i < serverMap.get(key); i++) { tempList.add(key); } } synchronized (index) { index++; if (index == tempList.size()) { index = 0; } return tempList.get(index); } }这次记录下了每台服务器的整体性能,给出一个数值,数值越大,性能越好。可以承受的请求也就越多,可以看到服务器 192.168.1.5 的性能为 4,是其他服务器的两倍,依旧 10 w 请求测试。 IP:192.168.1.3,次数:20000IP:192.168.1.2,次数:20000IP:192.168.1.5,次数:40000IP:192.168.1.4,次数:20000192.168.1.5 承担了 2 倍的请求。 随机加权随机加权的方式和轮训加权的方式大致相同,只是把使用互斥锁轮训的方式换成了随机访问,按照概率论来说,访问量增多时,服务访问也会达到负载均衡。 /* 服务器列表 /private static HashMap serverMap = new HashMap<>();static { serverMap.put("192.168.1.2", 2); serverMap.put("192.168.1.3", 2); serverMap.put("192.168.1.4", 2); serverMap.put("192.168.1.5", 4); }/** 加权路由算法*/ public static String randomWithWeight() { List<String> tempList = new ArrayList(); HashMap<String, Integer> tempMap = new HashMap<>(); tempMap.putAll(serverMap); for (String key : serverMap.keySet()) { for (int i = 0; i < serverMap.get(key); i++) { tempList.add(key); } } int randomInt = new Random().nextInt(tempList.size()); return tempList.get(randomInt); }依旧 10 w 请求测试,192.168.1.5 的权重是其他服务器的近似两倍, IP:192.168.1.3,次数:19934IP:192.168.1.2,次数:20033IP:192.168.1.5,次数:39900IP:192.168.1.4,次数:20133 IP-Hash上面的几种方式要么使用随机数,要么使用轮训,最终都达到了请求的负载均衡。但是也有一个很明显的缺点,就是同一个用户的多次请求很有可能不是同一个服务进行处理的,这时问题来了,如果你的服务依赖于 session ,那么因为服务不同, session 也会丢失,不是我们想要的,所以出现了一种根据请求端的 ip 进行哈希计算来决定请求到哪一台服务器的方式。这种方式可以保证同一个用户的请求落在同一个服务上。 private static List serverList = new ArrayList<>();static { serverList.add("192.168.1.2"); serverList.add("192.168.1.3"); serverList.add("192.168.1.4"); serverList.add("192.168.1.5"); } /** ip hash 路由算法*/ public static String ipHash(String ip) { // 复制遍历用的集合,防止操作中集合有变更 List<String> tempList = new ArrayList<>(serverList.size()); tempList.addAll(serverList); // 哈希计算请求的服务器 int index = ip.hashCode() % serverList.size(); return tempList.get(Math.abs(index)); } 总结上面的四种方式看似不错,那么这样操作下来真的体现了一开始说的负载均衡吗?答案是不一定的。就像上面的最后一个提问。 又或者每次出手的力度不同,有重有轻,恰巧熊 4 总是承受这种大力度啪啪打脸,熊 4 即将不省熊事,还要继续打它吗? 服务器也是这个道理,每次请求进行的操作对资源的消耗可能是不同的。比如说某些操作它对 CPU 的使用就是比较高,也很正常。所以负载均衡有时不能简单的通过请求的负载来作为负载均衡的唯一依据。还可以结合服务的当前连接数量、最近响应时间等维度进行总体均衡,总而言之,就是为了达到资源使用的负载均衡。 最后的话 文章已经收录在 Github.com/niumoo/JavaNotes ,欢迎Star和指教。更有一线大厂面试点,Java程序员需要掌握的核心知识等文章,也整理了很多我的文字,欢迎 Star 和完善,希望我们一起变得优秀。 原文地址https://www.cnblogs.com/niumoo/p/13021938.html
C# 根据BackgroundWorker异步模型和ProgressBar控件,自定义进度条控件 前言程序开发过程中,难免会有的业务逻辑,或者算法之类产生让人能够感知的耗时操作,例如循环中对复杂逻辑处理;获取数据库百万乃至千万级数据;http请求的时候等......用户在使用UI操作并不知道程序的内部处理,从而误操作导致程序无响应,关闭程序等待影响体验的情况,因此,在等待过程中提供友好的等待提示是有必要的,接下来我们一起封装一个自定义进度条控件! 主要使用技术(C#相关)BackgroundWoker异步模型ProgressBar控件泛型定时器 System.Timers.Timer自定义控件开发项目解决方案 BackgroundworkerEx : 自定义进度条控件工程Test : 调用BackgroundworkerEx的工程(只是展示如何调用)处理控件样式 新建一个ProgressbarEx名称的 用户控件添加Labal控件(lblTips),用于展示进度条显示的信息状态添加一个PictureBox控件(PicStop),充当关闭按钮,用于获取用户点击事件,触发关闭/终止进度条添加进度条ProgressBar控件(MainProgressBar)处理代码如下:进度条样式为"不断循环",并且速度为50该自定义用户控件不展示在任务栏中图片控件被点击事件------>设置当前属性IsStop=true,指示过程终止;TipMessage属性,用于设置进度条的信息SetProgressValue(int value) 设置进度条的Value属性,使得在ProgressBarStyle.Marquee样式中动画平滑MouseDown/MouseUp/MouseMove这三个事件是用于拖动无边框的用户控件(代码就不贴了)public ProgressbarEx(){ InitializeComponent(); MainProgressBar.Style = ProgressBarStyle.Marquee; MainProgressBar.MarqueeAnimationSpeed = 50; this.ShowInTaskbar = false; PicStop.Click += (s, eve) => { IsStop = true; }; this.MouseDown += CusProgressForm_MouseDown; this.MouseUp += CusProgressForm_MouseUp; this.MouseMove += CusProgressForm_MouseMove; } /// /// Need Stop ?/// public bool IsStop { get; private set; } = false; /// /// TipMessage/// public string TipMessage { get; set; } /// /// TipMessage/// public string TipMessage{ get { return lblTips.Text; } set { lblTips.Text = value; } } /// /// Set ProgressBar value ,which makes ProgressBar smooth/// /// public void SetProgressValue(int value){ if (MainProgressBar.Value == 100) MainProgressBar.Value = 0; MainProgressBar.Value += value; } 到现在,这个自定义进度条控件的样式基本完成了. 功能逻辑处理运行前所需定义BackgroundWorkerEx泛型类,并且继承于 IDisposable释放资源; /// <summary> /// Dispose /// </summary> public void Dispose() { try { DoWork = null; RunWorkCompleted = null; WorkStoped = null; _mWorkerThread = null; _mWorker.Dispose(); _mWorker = null; _mTimer = null; } catch (Exception){} } T用与异步处理的时候,传递T类型因为我们是通过.Net 的 BackgroundWorker异步模型来做的,所以我们理所当然定义相关的事件:异步开始异步完成加上我们自定义扩展的异步停止......报告进度事件在此进度条样式中并不需要我们先定义这四个事件所用到的参数,因为在BackgroundWorkerEx泛型类中,我们还是使用BackgroundWorker来处理异步过程,因此我们定义的参数泛型类需要继承原来的参数类型,并且在传输传递中,将原生BackgroundWorker的Argument,Result属性转成全局的泛型T,这样我们在外部调用的时候,拿到的返回结果就是我们传入到BackgroundWorkerEx泛型类中的T类型,而不需要使用as进行转换; 注:因为原生没有停止相关事件,所以自定义异步停止的事件参数使用的是DoWorkEventArgs public class DoWorkEventArgs<T> : DoWorkEventArgs { public new T Argument { get; set; } public new T Result { get; set; } public DoWorkEventArgs(object argument) : base(argument) { Argument = (T)argument; } } public class RunWorkerCompletedEventArgs<T> : RunWorkerCompletedEventArgs { public new T Result { get; set; } public RunWorkerCompletedEventArgs(object result, Exception error, bool cancelled) : base(result, error, cancelled) { Result = (T)result; } } 接着我们需要去定义事件,参数使用以上定义的泛型类 public delegate void DoWorkEventHandler(DoWorkEventArgs<T> Argument); /// <summary> /// StartAsync /// </summary> public event DoWorkEventHandler DoWork; public delegate void StopEventHandler(DoWorkEventArgs<T> Argument); /// <summary> /// StopAsync /// </summary> public event StopEventHandler WorkStoped; public delegate void RunWorkCompletedEventHandler(RunWorkerCompletedEventArgs<T> Argument); /// <summary> /// FinishAsync /// </summary> public event RunWorkCompletedEventHandler RunWorkCompleted; 定义全局的字段private BackgroundWorker _mWorker = null;异步操作必要;private T _mWorkArg = default(T);操作传递进来的参数类并且返回到外部private Timer _mTimer; 定时器检测自定义进度条控件属性IsStop是否为true,并且动态修改进度条消息private Thread _mWorkerThread = null;异步操作在该线程中,终止时调用About()抛出ThreadAbortException异常,用于标记当前是停止而不是完成状态private int _miWorkerStartDateSecond = 0; 异步消耗时间(非必要)private int _miShowProgressCount = 0; 动态显示"."的个数(非必要)private ProgressbarEx _mfrmProgressForm = null; 自定义进度条控件实例 /// <summary> /// .Net BackgroundWorker /// </summary> private BackgroundWorker _mWorker = null; /// <summary> /// Whole Para /// </summary> private T _mWorkArg = default(T); /// <summary> /// Timer /// </summary> private Timer _mTimer = null; /// <summary> /// WorkingThread /// </summary> private Thread _mWorkerThread = null; /// <summary> /// Async time sec /// </summary> private int _miWorkerStartDateSecond = 0; /// <summary> /// Async time dot /// </summary> private int _miShowProgressCount = 0; /// <summary> /// ProgressbarEx /// </summary private ProgressbarEx _mfrmProgressForm = null; 定义全局属性IsBusy 返回_mWorker的工作忙碌是否ProgressTip 自定义进度条控件显示内容 /// <summary> /// Express Busy /// </summary> public bool IsBusy { get { if (_mWorker != null) { return _mWorker.IsBusy; } return false; } } /// <summary> /// 进度条提示 默认: 正在加载数据,请稍后[{0}]{1} /// </summary> public string ProgressTip { get; set; } = "Elapsed Time[{0}]{1}"; 到现在,我们已经将必要的字段,属性,样式都处理完成!!! 接下来我们就要实现方法 方法实现异步工作事件,用法与BackgroundWorker一致,如果调用处没有注册DoWork事件,则直接返回将接受到的参数创建成泛型参数类开线程,将异步操作放在该线程中操作,注意设置线程的IsBackground=true,防止主进程意外退出,线程还在处理循环直到线程结束e.Result = Argument.Result;将结果赋予Result,在停止或者完成事件中可以获取到结果 /// <summary> /// Working /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Worker_DoWork(object sender, DoWorkEventArgs e) { if (DoWork == null) { e.Cancel = true; return; } DoWorkEventArgs<T> Argument = new DoWorkEventArgs<T>(e.Argument); try { if (_mWorkerThread != null && _mWorkerThread.IsAlive) { _mWorkerThread.Abort(); } } catch (Exception) { Thread.Sleep(50); } _mWorkerThread = new Thread(a => { try { DoWork?.Invoke(a as DoWorkEventArgs<T>); } catch (Exception) { } }); _mWorkerThread.IsBackground = true; _mWorkerThread.Start(Argument); //Maybe cpu do not start thread Thread.Sleep(20); //Wait..... while (_mWorkerThread.IsAlive) { Thread.Sleep(50); } e.Result = Argument.Result; } 异步完成/停止当线程停止抛出异常(catch但是不处理)/线程完成时会进入异步完成事件 完成后,将自定义进度条控件实例关闭,释放将全局的BackgroundWorker实例_mWorker相关事件取消注册,并且检查线程情况感觉线程情况,如果线程状态为ThreadState.Aborted意味着线程被停止了,调用停止事件,否则调用完成事件 /// <summary> /// Completed /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Worker_RunWorkCompleted(object sender, RunWorkerCompletedEventArgs e) { try { if (_mfrmProgressForm != null) { _mfrmProgressForm.Close(); _mfrmProgressForm.Dispose(); _mfrmProgressForm = null; } if (_mWorker != null) { _mWorker.DoWork -= Worker_DoWork; _mWorker.RunWorkerCompleted -= Worker_RunWorkCompleted; try { if (_mWorkerThread != null && _mWorkerThread.IsAlive) _mWorkerThread.Abort(); } catch (Exception) { } } //In timer, When stop progress will make thread throw AbortException if (_mWorkerThread != null && _mWorkerThread.ThreadState == ThreadState.Aborted) { WorkStoped?.Invoke(new DoWorkEventArgs<T>(_mWorkArg)); } else { RunWorkCompleted?.Invoke(new RunWorkerCompletedEventArgs<T>(e.Result, e.Error, e.Cancelled)); } } catch (Exception ex) { throw ex; } } 线程开始检查消息提醒内容 , {0}{1}同于显示异步耗时和".."的个数在定时器执行方法中,检查_mfrmProgressForm.IsStop是否为true,这个属性标志是否被停止;true则抛出异常_mfrmProgressForm不为Null则不断修改当前的内容提醒,友好化,实际可以按需处理 /// <summary> /// Timer Start /// </summary> private void StartTimer() { //Check user ProgressTip if (!ProgressTip.Contains("{0}")) { ProgressTip += "...Elapsed Time{0}{1}"; } if (_mTimer != null) return; //On one sec _mTimer = new Timer(1000); _mTimer.Elapsed += (s, e) => { //progress and it's stop flag (picture stop)|| this stop flag if (_mfrmProgressForm != null && _mfrmProgressForm.IsStop) { if (_mWorker != null) { try { if (_mWorkerThread != null && _mWorkerThread.IsAlive) { if (_mTimer != null && _mTimer.Enabled) { _mTimer.Stop(); _mTimer = null; } _mWorkerThread.Abort(); } } catch (Exception) { } } } if (_mfrmProgressForm != null) { //Callback _mfrmProgressForm.Invoke(new Action<DateTime>(elapsedtime => { DateTime sTime = elapsedtime; //worked time _miWorkerStartDateSecond++; if (_mfrmProgressForm != null) { _mfrmProgressForm.SetProgressValue(_miWorkerStartDateSecond); } //.....count _miShowProgressCount++; if (_miShowProgressCount > 6) { _miShowProgressCount = 1; } string[] strs = new string[_miShowProgressCount]; string ProgressStr = string.Join(".", strs); string ProgressText = string.Format(ProgressTip, _miWorkerStartDateSecond, ProgressStr); if (_mfrmProgressForm != null) { _mfrmProgressForm.TipMessage = ProgressText; } }), e.SignalTime); } }; if (!_mTimer.Enabled) { _mTimer.Start(); } } 最后一步:异步开始 与BackgroundWorker用法一致,只是在最后开始了定时器和进度条控件而已/// /// Start AsyncWorl /// </summary> /// <param name="Para"></param> public void AsyncStart(T Para) { //if workeven is null ,express user do not regist event if (DoWork == null) { return; } _miWorkerStartDateSecond = 0; _miShowProgressCount = 0; //init if (_mWorker != null && _mWorker.IsBusy) { _mWorker.CancelAsync(); _mWorker = null; } _mWorker = new BackgroundWorker(); //create progressbar _mfrmProgressForm = new ProgressbarEx(); //add event _mWorker.DoWork += Worker_DoWork; _mWorker.RunWorkerCompleted += Worker_RunWorkCompleted; _mWorker.WorkerReportsProgress = true; _mWorker.WorkerSupportsCancellation = true; //Set Whole Para _mWorkArg = Para; _mWorker.RunWorkerAsync(Para); //Start timer StartTimer(); _mfrmProgressForm.StartPosition = FormStartPosition.CenterParent; _mfrmProgressForm.ShowDialog(); } 到这里,整个的进度条控件已经完成了! 调用定义一个参数类 /// <summary> /// Para Class /// </summary> public class ParaArg { public DataTable Data { get; set; } public string Msg { get; set; } public Exception Ex { get; set; } } 定义全局的帮助类BackgroundWorkerEx workHelper = null;调用 if (workHelper != null || (workHelper != null && workHelper.IsBusy)) { workHelper.Dispose(); workHelper = null; } if (workHelper == null) { workHelper = new BackgroundWorkerEx<ParaArg>(); } workHelper.DoWork += (eve) => { ParaArg args = eve.Argument; try { //ToDo like Thread.Sleep(20000); Thread.Sleep(10000); args.Msg = "...this is bussiness code result"; throw new Exception(""); } catch (Exception ex) { args.Ex = ex; } finally { eve.Result = args; } }; workHelper.RunWorkCompleted += (eve) => { if (eve.Error != null) { //get .net backgroundworker exception; //handle this exception; //return ? } //get your para result ParaArg x = eve.Result; if (x.Ex != null) { //get your bussiness exception; //handle this exception; //return ? } //finially get your need; //MayBe to do some UI hanlde and bussiness logical string sReusltMsg = x.Msg; }; workHelper.WorkStoped += (eve) => { //if stoped ! it means no error; //just get what you want; ParaArg x = eve.Result as ParaArg; btnBegin.Enabled = true; }; //参数 ParaArg arg = new ParaArg() { Msg = "Msg" }; workHelper.AsyncStart(arg); 最后其实不管是封装的过程,还是调用,可以说完全就是BackgroundWorker的方式,所以很多BackgroundWorker相关的地方我都没有很详细的去说明;只要看看这个异步模型,就能够很好理解!大家有空也可以实现以下,有问题也可以详细,我比较喜欢交流技术~~~ 还有一点就是这个解决方案已经放上Github上了,欢迎大家拉下来用 GitHub地址 欢迎star/fork原文地址https://www.cnblogs.com/Ligy97/p/12993155.html
基于bin-log&position搭建主从架构MySQL 目录一、MySQL主从搭建二、主库2.1、确定主库的binlog是否开启2.2、骚气的命令2.3、记录主库的master状态三、从库3.1、从库和主库保持同步3.2、开启主从同步3.3、从库:如何断开主从3.4、主库:如何断开主从四、中断处理4.1、Slave_IO_Running异常4.2、Slave_Sql_Running异常五、流程六、可能会遇到的问题6.1、问题一:6.2、问题二:6.3、问题三:6.4、问题四:6.5、问题五:6.6、问题六:6.7、问题七:一、MySQL主从搭建#搭建主从架构的MySQL常用的有两种实现方式: 基于binlog的fileName + postion模式完成主从同步。基于gtid完成主从同步搭建。本篇就介绍如何使用第一种方式完成MySQL主从环境的搭建。 基于fileName和position去实现主从复制,所谓的fileName就是bin-log的name,position指的是slave需要从master的binlog的哪个位置开始同步数据。 这种模式同步数据方式麻烦的地方就是需要我们自己通过如下的命令去查找应该从哪个bin-log的哪个position去开始同步。 二、主库#2.1、确定主库的binlog是否开启#命令:show variables like 'bin-log' 原因:了解MySQL中常见的三个日志: 单机MySQL的undolog日志中记录着如何将现有的数据恢复成被修改前的旧数据。单机MySQL的redolog. 中记录事物日志。主从模式的MySQL通过bin-log日志同步数据。2.2、骚气的命令#Copygrant replication slave on . to MySQLsync@"127.0.0.1" identified by "MySQLsync123";这条命令是在干什么呢? 捋一下思路:我们做主从同步,在主库这边我们其实会单独创建一个账号用于实现主从同步。下面的命令其实就会帮我们创建出 username=mysqlsync password=mysqlsync123的账户专门用户主从同步使用。 执行完上面的命令后,执行如下的命令查看上面的grant执行结果: Copyselect user, host from mysql.user like '%mysqlsync%' 2.3、记录主库的master状态#注意主库的查看主库当前是第几个binlog,已经数据的position。 因为一会从库就是根据这两个信息知道自己该从主库的第几个binlog的什么positon开始同步。 三、从库#3.1、从库和主库保持同步#从库执行change语句,和主库保持同步 CopyCHANGE MASTER TO MASTER_HOST='10.157.23.158', MASTER_USER='mysqlsync', MASTER_PASSWORD='mysqlsync123', MASTER_PORT=8882, MASTER_LOG_FILE='mysql-bin.000008', MASTER_LOG_POS=1013; CHANGE MASTER TO MASTER_HOST = '${new_master_ip}', MASTER_USER = '${user}', MASTER_PASSWORD = '${password}', MASTER_PORT = ${new_master_port}, master_auto_position = 1; CHANGE MASTER TO MASTER_HOST = '10.157.23.123', MASTER_USER = 'mysqlsync', MASTER_PASSWORD = 'mysqlsync123', MASTER_PORT = 8882, master_auto_position = 1; 3.2、开启主从同步#Copystart slaveshow slave status G 当我们可以看到 io线程和sql线程的状态都是yes时,说明此刻主从同步已经搭建完成了。 3.3、从库:如何断开主从#Copystop slave io_threadstop slave sql_thread3.4、主库:如何断开主从#把用于进行主从同步的账号删除就好了 Copydrop user ${user}@${slave_ip} 四、中断处理#中断处理部分说的是,一开始我们搭建主从很可能并不是一番风顺的,就比如上面的Slave_IO_Running和Slave_SQL_Running很可能处于NO的状态。下面介绍一下常见的解决方式。 4.1、Slave_IO_Running异常#Slave_IO_Running:no/connecting 这说明从库连接不上主库,或者是一直处于正在连接的状态。 可能是主库没有对从库进行授权,如果已经授权了那么重启一下salve。 另一种原因就是master和slave的mysqld相关配置文件中,配置了相同server_id。 还有可能你在执行change master命令时,输入的主库相关的信息本来就是错误的。 4.2、Slave_Sql_Running异常#Slave_Sql_Running:no 一般这种情况是bin-log中的sql出问题了。 第一种情况:可能我们配置了slave只能读,但是却有写请求打过来了,导致slave不能继续往下执行。 第二种解决思路:让slave跳过有问题的这个事件,但是还是得把事件的原因查明白,不然不推荐直接跳过这个事件。 Copystop slave;set global sql_slave_skip_counter=1;start slave;第三种思路:我们提前配置好错误号机制,当slave在同步的过程中,碰到我们配置的错误号采取自动跳过的机会而不再去默认的终止同步数据。 Copy 一般我们可以像下面这样,在my.cnf中的[MySQLd]的启动参数中添加如下内容 --slave-skip-errors=1062,1053 --slave-skip-errors=all --slave-skip-errors=ddl_exist_errors 通过如下语句查看当前MySQL配置的变量 MySQL> show variables like 'slave_skip%'; 通过如下命令可以查看到出现的errorno show slave status; # 观察Last_Errno 常见的errorno 1007:数据库已存在,创建数据库失败1008:数据库不存在,删除数据库失败1050:数据表已存在,创建数据表失败1051:数据表不存在,删除数据表失败1054:字段不存在,或程序文件跟数据库有冲突1060:字段重复,导致无法插入1061:重复键名1068:定义了多个主键1094:位置线程ID1146:数据表缺失,请恢复数据库1053:复制过程中主服务器宕机1062:主键冲突 Duplicate entry '%s' for key %d第四种思路:手动给slave调整fileName和position的位置(如何允许放弃之前的一部分数据,而从当前最新的数据开始同步) Copy 停掉slave slave stop 进入master 停止master的写操作 查看master中当前bin-log和position show master status; 切换回slave从新根据最新的position和bin-log进行同步 进入master,开启master的写操作 五、流程#通过fileName和position完成定位,从库会向主库发送命令,BINLOG_DUMP ,命令中包含有positon和fileName, 主库获取到这些信息之后,指定name到指定position往从库发送bin-log 六、可能会遇到的问题#6.1、问题一:#change master时报错了 报错说:ERROR 1776 (HY000): Parameters MASTER_LOG_FILE, MASTER_LOG_POS, RELAY_LOG_FILE and RELAY_LOG_POS cannot be set when MASTER_AUTO_POSITION is active. 原因是我之前使用过gtid进行同步数据,当时将master_auto_position设置成了1,再想使用手动指定position的主从同步方式需要得像下面这样,change回去。 CopyCHANGE MASTER TO MASTER_AUTO_POSITION=0; 6.2、问题二:#如果我随便写了个position再搭建主从时,会发生什么? 下面的 MASTER_LOG_POS = 1003 就是我随便写的一个position,然后你可以看到两个现象 Slave_IO_Running : NoLast_IO_Error 位置报了个严重的错误Copymysql> CHANGE MASTER TO -> MASTER_HOST='10.157.23.xxx', -> MASTER_USER='mysqlsync', -> MASTER_PASSWORD='mysqlsync123', -> MASTER_PORT=8882, -> MASTER_LOG_FILE='mysql-bin.000008', -> MASTER_LOG_POS=1003; Query OK, 0 rows affected, 2 warnings (0.01 sec) mysql> start slave;Query OK, 0 rows affected (0.00 sec) mysql> show slave statusG; 1. row ** Slave_IO_State: Master_Host: 10.157.23.158 Master_User: mysqlsync Master_Port: 8882 Connect_Retry: 60 Master_Log_File: mysql-bin.000008 Read_Master_Log_Pos: 1003 Relay_Log_File: relay-log.000002 Relay_Log_Pos: 320 Relay_Master_Log_File: mysql-bin.000008 Slave_IO_Running: No Slave_SQL_Running: Yes Replicate_Do_DB: Replicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table: mysql.%,test.% Last_Errno: 0 Last_Error: Skip_Counter: 0 Exec_Master_Log_Pos: 1003 Relay_Log_Space: 521 Until_Condition: None Until_Log_File: Until_Log_Pos: 0 Master_SSL_Allowed: No Master_SSL_CA_File: Master_SSL_CA_Path: Master_SSL_Cert: Master_SSL_Cipher: Master_SSL_Key: Seconds_Behind_Master: NULL Master_SSL_Verify_Server_Cert: No Last_IO_Errno: 1236 Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'log event entry exceeded max_allowed_packet; Increase max_allowed_packet on master; the first event 'mysql-bin.000008' at 1003, the last event read from './mysql-bin.000008' at 123, the last byte read from './mysql-bin.000008' at 1022.' Last_SQL_Errno: 0 Last_SQL_Error: Replicate_Ignore_Server_Ids: Master_Server_Id: 2787871625 Master_UUID: a5f1d6b2-8f9a-11ea-8138-b8599f2ef058 Master_Info_File: mysql.slave_master_info SQL_Delay: 0 SQL_Remaining_Delay: NULL Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates Master_Retry_Count: 86400 Master_Bind: Last_IO_Error_Timestamp: 200529 10:22:46 Last_SQL_Error_Timestamp: Master_SSL_Crl: Master_SSL_Crlpath: Retrieved_Gtid_Set: Executed_Gtid_Set: 00c755a6-7a07-11ea-8701-b8599f2ef058:33-222, 40efcb1b-7a1f-11ea-84ac-b8599f229b38:1-20,7e2dcb21-7d3b-11ea-aa0c-b8599f2ef058:1-18,9e6027f2-7ae9-11ea-ac13-b8599f2ef058:1409-7176,a5f1d6b2-8f9a-11ea-8138-b8599f2ef058:6-9:12-13:15,e90fdd54-7e04-11ea-8b23-b8599f2ef058:1-11 Auto_Position: 0 Replicate_Rewrite_DB: Channel_Name: Master_TLS_Version: 1 row in set (0.00 sec)6.3、问题三:#假设我们有这样的场景: 场景:现在主库有7条数据,从库有5条数据,搭建主从时如何让从库从第六条开始同步? 这种情况仅仅是我们在做这种小实验,为啥这样说呢?如果是为线上的业务搭建搭建主从MySQL的话,大概率我们会清空主库然后再做同步。如果数据很重要,我们会对主库中的数据进行一次全量拷贝到从库(拷贝var包)。再做主从同步。 在线上的环境中,主从的数据是会强一致的,从库只会接受业务方的读流量,也许网络环境很恶劣从库同步的速度明显比主库写入到速度低,但是只要从库没有说跳过了某个binlog而少同步了某条记录,我们都可以认为它们是正常的主从同步。不会出现主从中断的情况。 线上的环境中什么情况下会出现主从中断呢?比如说,从库同步数据时,从库同步binlog时丢了一条数据,这时业务上突然来了条update语句,要更新数据,然后从库美滋滋的回放在主库dump过来的binlog时发现,竟然自己没有需要更新的这条记录,就会报错,这时为了业务止损,我们要在第一时间下线从库,然后去分析哪里出现问题了。 针对这个实验我们这样去binlog中查看第5,6条数据的position,然后在从库中使用相应的position完成主从数据的同步。 进入主库,通过下面的命令查看binlog Copymysqlbinlog --no-defaults -vv --base64-output=decode-rows ../var/mysql-bin.000008 | less 找到了指定的binlog和指定的end_log_pos 比如从库中没有第10,11条数据,我们就能通过end_log_pos = postion = 1013完成定位。 CopyCHANGE MASTER TO MASTER_HOST='10.157.23.158', MASTER_USER='mysqlsync', MASTER_PASSWORD='mysqlsync123', MASTER_PORT=8882, MASTER_LOG_FILE='mysql-bin.000008', MASTER_LOG_POS=1013; 开启同步,并查看状态 Copystart slave;show slave statusG;再去查看从库就能发现,从你指定的position开始往后和主库的数据保持同步的。 6.4、问题四:#问:主从接流量的情况是怎样的?业务的CRUD请求是如何被主从平分消费的? 答:默认这种架构下是读写分离,也就是说,仅读流量会打到从库中 问:那如果我们在从库所在的机器上本地登陆,然后手动执行删除的操作能成功吗? 答:是的,可以执行成功。 问:我可以简单粗暴的限制从库仅读吗? 答:可以的,像下面这样 Copy mysql> show variables like '%read_only%'; Variable_name Value innodb_read_only OFF read_only OFF super_read_only OFF transaction_read_only OFF tx_read_only OFF 5 rows in set (0.00 sec) set global read_only=0; #关闭只读,可以读写set global read_only=1; #开始只读模式6.5、问题五:#假设主库中有1~7 共7条数据,从库中有1~5五条数据。也就是说,主库从库中前五条数据一样,但是主库比从库多了两条新数据。 这时我们搭建主从同步时搞一搞事情,重复这个动作:在从库断开同步,然后查到主库第一个binlog中的数据的记录,确定我们要查找的position,再重新构建主从环境。观察一下从库这边数据的同步情况,以及会出现什么问题?从库这边的数据会成为double吗? 答:数据不会double的 6.6、问题六:#假设从库执行changemaster时,主库MASTER_HOST填错了: 在查看slave 状态时,我们可以看到Last_IO_Error列有报错提示: error connecting to master Copymysql> CHANGE MASTER TO -> MASTER_HOST='10.157.23.158', -> MASTER_USER='mysqlsync', -> MASTER_PASSWORD='mysqlsync123', -> MASTER_PORT=8882, -> MASTER_LOG_FILE='mysql-bin.000008', -> MASTER_LOG_POS=1013; Query OK, 0 rows affected, 2 warnings (0.01 sec) mysql> start slave;Query OK, 0 rows affected (0.00 sec) mysql> show slave statusG; 1. row ** Slave_IO_State: Connecting to master Master_Host: 10.157.23.123 Master_User: mysqlsync Master_Port: 8882 Connect_Retry: 60 Master_Log_File: mysql-bin.000008 Read_Master_Log_Pos: 1003 Relay_Log_File: relay-log.000001 Relay_Log_Pos: 4 Relay_Master_Log_File: mysql-bin.000008 Slave_IO_Running: Connecting Slave_SQL_Running: Yes Replicate_Do_DB: Replicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table: mysql.%,test.% Last_Errno: 0 Last_Error: Skip_Counter: 0 Exec_Master_Log_Pos: 1003 Relay_Log_Space: 154 Until_Condition: None Until_Log_File: Until_Log_Pos: 0 Master_SSL_Allowed: No Master_SSL_CA_File: Master_SSL_CA_Path: Master_SSL_Cert: Master_SSL_Cipher: Master_SSL_Key: Seconds_Behind_Master: NULL Master_SSL_Verify_Server_Cert: No Last_IO_Errno: 2003 Last_IO_Error: error connecting to master 'mysqlsync@10.157.23.123:8882' - retry-time: 60 retries: 1 Last_SQL_Errno: 0 Last_SQL_Error: Replicate_Ignore_Server_Ids: Master_Server_Id: 0 Master_UUID: Master_Info_File: mysql.slave_master_info SQL_Delay: 0 SQL_Remaining_Delay: NULL Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates Master_Retry_Count: 86400 Master_Bind: Last_IO_Error_Timestamp: 200529 10:13:34 Last_SQL_Error_Timestamp: Master_SSL_Crl: Master_SSL_Crlpath: Retrieved_Gtid_Set: Executed_Gtid_Set: 00c755a6-7a07-11ea-8701-b8599f2ef058:33-222, 40efcb1b-7a1f-11ea-84ac-b8599f229b38:1-20,7e2dcb21-7d3b-11ea-aa0c-b8599f2ef058:1-18,9e6027f2-7ae9-11ea-ac13-b8599f2ef058:1409-7176,a5f1d6b2-8f9a-11ea-8138-b8599f2ef058:6-9:12-13:15,e90fdd54-7e04-11ea-8b23-b8599f2ef058:1-11 Auto_Position: 0 Replicate_Rewrite_DB: Channel_Name: Master_TLS_Version: 6.7、问题七:#假设这种场景:假设主从现在的数据是一致的,然后你在从库所在的机器上本地登陆,然后手动删除一条,再从主库写入数据,那从库还能同步成功吗? 答:从库依然会同步成功,但是其实这时候已经算是事故了,主从数据不一致,万一业务打来一条sql刚好使用你删的数据,那就会报错。 如果觉得对你有帮助欢迎关注我,后面还会分享通过gtid搭建主从mysql以及其他相关的知识点 作者: 赐我白日梦 出处:https://www.cnblogs.com/ZhuChangwu/p/12990062.html
换个角度学习ASP.NET Core中间件 中间件真面目关于ASP.NET Core中间件是啥,简单一句话描述就是:用来处理HTTP请求和响应的一段逻辑,并且可以决定是否把请求传递到管道中的下一个中间件! 上面只是概念上的一种文字描述,那问题来了,中间件在程序中到底是个啥❓ 一切还是从IApplicationBuilder说起,没错,就是大家熟悉的Startup类里面那个Configure方法里面的那个IApplicationBuilder(有点绕,抓住重点就行)。 IApplicationBuilder,应用构建者,听这个名字就能感受它的核心地位,ASP.NET Core应用就是依赖它构建出来,看看它的定义: public interface IApplicationBuilder{ //...省略部分代码... IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); RequestDelegate Build(); }Use方法用来把中间件添加到应用管道中,此时我们已经看到中间件的真面目了,原来是一个委托,输入参数是RequestDelegate,返回也是RequestDelegate,其实RequestDelegate还是个委托,如下: public delegate Task RequestDelegate(HttpContext context);还记得中间件是干嘛的吗?是用来处理http请求和响应的,即对HttpContext的处理,这里我们可以看出来原来中间件的业务逻辑就是封装在RequestDelegate里面。 总结一下: middleware就是Func,输入的是下一个中间件的业务处理逻辑,返回的就是当前中间件的业务处理逻辑,并在其中决定要不要调用下个中间件!我们代码实现一个中间件看看(可能和我们平时用的不太一样,但它就是中间件最原始的形式!): //Startup.Configure方法中Func middleware1 = next => async (context) => { //处理http请求 Console.WriteLine("do something before invoke next middleware in middleware1"); //调用下一个中间件逻辑,当然根据业务实际情况,也可以不调用,那此时中间件管道调用到此就结束来了! await next.Invoke(context); Console.WriteLine("do something after invoke next middleware in middleware1"); }; //添加到应用中 app.Use(middleware1); 跑一下瞅瞅,成功执行中间件! IIS Express is running.info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: E:\vs2019Project\WebApplication3\WebApplication3 do something before invoke next middleware in middleware1do something after invoke next middleware in middleware1中间件管道通过上面我们有没有注意到,添加中间时,他们都是一个一个独立的被添加进去,而中间件管道就是负责把中间件串联起来,实现下面的一个中间件调用流转流程: 如何实现呢?这个就是IApplicationBuilder中的Build的职责了,再次看下定义: public interface IApplicationBuilder{ //...省略部分代码... IApplicationBuilder Use(Func middleware); RequestDelegate Build();}Build方法一顿操作猛如虎,主要干一件事把中间件串联起来,最后返回了一个 RequestDelegate,而这个就是我们添加的第一个中间件返回的RequestDelegate, 看下框架默认实现: //ApplicationBuilder.cspublic RequestDelegate Build() { RequestDelegate app = context => { // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. var endpoint = context.GetEndpoint(); var endpointRequestDelegate = endpoint?.RequestDelegate; if (endpointRequestDelegate != null) { var message = $"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " + $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + $"routing."; throw new InvalidOperationException(message); } context.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var component in _components.Reverse()) { app = component(app); } return app; } Build方法里面定义了一个 RequestDelegate ,作为最后一个处理逻辑,例如返回404。_components存储着添加的所有中间件中间件管道调度顺序,就是按照中间添加的顺序调用,所以中间件的顺序很重要,很重要,很重要!遍历_components,传入next RequestDelegate,获取当前RequestDelegate,完成管道构建!中间件使用在此之前,还是提醒下,中间件最原始的使用姿势就是 IApplicationBuilder Use(Func middleware);下面使用的方式,都是对此方式的扩展! Lamda方式大多数教程里面都提到的方式,直接上代码: //扩展方法//IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware)app.Use(async (context, next) => { Console.WriteLine("in m1"); await next.Invoke(); Console.WriteLine("out m1"); }); 扩展方法简化了中间件的使用,这个里面就只负责写核心逻辑,然后扩展方法中把它包装成Func类型进行添加,不像原始写的那样复杂,我们看下这个扩展方法实现,哈,原来就是一个简单封装!我们只要专注在middleware里面写核心业务逻辑即可。 public static IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware) { return app.Use(next => { return context => { Func<Task> simpleNext = () => next(context); return middleware(context, simpleNext); }; }); } 如果我们定义中间件作为终端中间件(管道流转此中间件就结束了,不再调用后面的中间件)使用时,上面只要不调用next即可。 当然我们还有另外一个选择,自己使用扩展Run方法,传入的参数就是RequestDelegate,还是上代码: //扩展方法//public static void Run(this IApplicationBuilder app, RequestDelegate handler);app.Run(async (context) => { Console.WriteLine("in m3"); await context.Response.WriteAsync("test22"); Console.WriteLine("out m3"); }); 到此,我们有没有发现上面的方式有些弊端,只能处理下简单逻辑,如果要依赖第三方服务,那可怎么办? 定义中间件类方式使用中间件类,我们只要按照约定的方式,即类中包含InvokeAsync方法,就可以了。 使用类后,我们就可以注入我们需要的第三方服务,然后完成更复杂的业务逻辑,上代码 //定义第三方服务public interface ITestService { Task Test(HttpContext context); } public class TestService : ITestService { private int _times = 0; public Task Test(HttpContext context) { return context.Response.WriteAsync($"{nameof(TestService)}.{nameof(TestService.Test)} is called {++_times} times\n"); } } //添加到IOC容器public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITestService, TestService>(); } //中间件类,注入ITestServicepublic class CustomeMiddleware1 { private int _cnt; private RequestDelegate _next; private ITestService _testService; public CustomeMiddleware1(RequestDelegate next, ITestService testService) { _next = next; _cnt = 0; _testService = testService; } public async Task InvokeAsync(HttpContext context) { await _testService?.Test(context); await context.Response.WriteAsync($"{nameof(CustomeMiddleware1)} invoked {++_cnt} times"); } } //添加中间件,还是一个扩展方法,预知详情,请看源码app.UseMiddleware();运行一下,跑出来的结果如下,完美! 等一下,有没有发现上面有啥问题???❓ 明明ITestService是以Transient注册到容器里面,应该每次使用都是新实例化的,那不应该被显示被调用 15 次啊!!! 这个时候我们应该发现,我们上面的所有方式添加的中间件的生命周期其实和应用程序是一致的,也就是说是只在程序启动的时候实例化一次!所以这里第三方的服务,然后以Transient方式注册到容器,但在中间件里面变现出来就是一个单例效果,这就为什么我们不建议在中间件里面注入DbContext了,因为DbContext我们一般是以Scoped来用的,一次http请求结束,我们就要释放它! 如果我们就是要在中间件里面是有ITestService,而且还是Transient的效果,怎么办? 实现IMiddleware接口//接口定义public interface IMiddleware{ ask InvokeAsync(HttpContext context, RequestDelegate next);}//实现接口public class CustomeMiddleware : IMiddleware { private int _cnt; private ITestService _testService; public CustomeMiddleware(ITestService testService) { _cnt = 0; _testService = testService; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { await _testService?.Test(context); await context.Response.WriteAsync($"{nameof(CustomeMiddleware)} invoked {++_cnt} times"); } } //添加中间件app.UseMiddleware();运行一下,结果报错了... ,提示CustomeMiddleware没有注册! InvalidOperationException: No service for type 'WebApplication3.CustomeMiddleware' has been registered.通过报错信息,我们已经知道,如果实现了IMiddleware接口的中间件,他们并不是在应用启动时就实例化好的,而是每次都是从IOC容器中获取的,其中就是IMiddlewareFactory 来解析出对应类型的中间件的(内部就是调用IServiceProvider),了解到此,我们就知道,此类中间件此时是需要以service的方式注册到IOC容器里面的,这样中间件就可以根据注册时候指定的生命周期方式来实例化,从而解决了我们上一节提出的疑问了!好了,我们注册下中间件服务 public void ConfigureServices(IServiceCollection services) { services.AddTransient<CustomeMiddleware>(); services.AddTransient<ITestService, TestService>(); } 再次多次刷新请求,返回都是下面的内容 TestService.Test is called 1 timesCustomeMiddleware invoked 1 times结语中间件存在这么多的使用方式,每一个存在都是为了解决实际需求的,当我们了解这些背景知识后,在后面自己使用时,就能更加的灵活! 作者:小伟06 出处:https://www.cnblogs.com/liuww/p/12944937.html
Linux 目录详解 和 Windows 一样,Linux 也有目录的概念。系统中众多的文件需要借助到目录进行存储与管理。我们下面对 Linux 系统中的目录结构以及经常使用到的概念进行一个完整介绍。Linux 系统目录结构 在 Windows下,系统把硬盘划分为不同的磁盘分区,每个磁盘分配一个不同的盘符,比如 C:、D: 等等。每个分区都单独存放自己的文件,有自己的根目录,比如 C:、D: 就相当于 C 盘和 D 盘的根目录了。而 Linux 则是将整个硬盘统一管理,整个系统只有一个根目录 "/",所有的文件和目录都是在这个根目录下,它表示 Linux 文件系统的起点。另外要注意一点,Linux 中目录间的间隔使用的是斜线 (forward slash)"/",而 Windows中用的是反斜线 (back slash)""。 为了规范,绝大多数的 Linux 发行版本中 (参见 《这么多Linux版本,你究竟该怎么选择?》)系统目录的机构都保持了一致。以 CentOS 7 为例,下面介绍一下常用的系统目录和它们的作用。 /root:超级用户 root 的家目录/bin:是 Binary 的缩写,用于存放系统中常用的命令,任何用户都有权限执行/boot:存放系统启动时所需要的文件,Linux 的内核就存放在这个目录中/dev:是 Device 的缩写,存放硬盘、光驱、鼠标等设备文件,在 Linux中访问设备和访问文件的方式是相同的/etc:存放系统和程序所需的配置文件,作用类似于 Windows 的注册表/home:普通用户的家目录。普通用户的家目录一般和用户账号名相同,比如 user1 的家目录是 /home/user1/var:存放一些经常变化的文件,比如 /var/log 存放日志,/var/spool/mail 存放邮件/lib /lib64:存放连接共享库,作用类似于 Windows 的 dll 文件。它们自己不能被执行,需要被其他的程序调用,几乎所有的应用程序都需要用到共享库/usr:软件默认的安装位置,存放用户应用程序和文件,类似于 Windows 下的 Program Files/media:用于挂载 U 盘、光驱等系统自动识别的设备/lost+found:存放系统崩溃或意外关机时产生的碎片文件,正常情况下是空的/mnt:用于临时挂载别的文件系统,比如增加一块硬盘,需要手动挂载/opt:用于安装额外软件所用的目录,默认是空的/proc:虚拟文件系统,是系统内存的映射。访问这个目录可以获取很多有用的系统信息/sbin:s 就是 super 的意思,存放只有系统管理员才有权限执行的命令/srv:存放一些服务启动后所需要提取的数据/sys:该目录下安装了文件系统 sysfs,该文件系统是内核设备树的一个直观反映/tmp:存放临时文件,所有用户都可执行写操作/run:存放进程产生的临时文件,系统重启以后,这个目录会被清空绝对路径与相对路径 1) 绝对路径:将目录用完整的路径表示出来,从根目录“/”开始,比如 /user/bin,使用绝对路径可以非常准确的表示一个目录的位置,只不过路径较长的时候,输入会比较麻烦。 下面看一个例子,当以用户 user1 登录后,使用 pwd (print working directory)命令来打印出当前的工作目录 $ pwd /home/user1 /home/user1 就是当前工作目录的绝对路径,也是 user1 的家目录,如上面介绍,普通用户的家目录都是在 /home 下面。 2) 相对路径:顾名思义,就是不用绝对路径表示,而是用当前工作目录为起点来表示的相对路径,比如当前工作目录为 /home/user1,/home/user1/Desktop 用相对路径表示即为 ./Desktop 或者直接 Desktop,“.”表示当前工作目录,“..”表示上一级目录 $ cd Desktop $ pwd /home/user1/Desktop 常用的目录操作 1) cd:change directory 切换工作目录,语法格式为 $ cd [目录名] 2) pwd:print working directory 打印当前工作目录,语法格式为 $ pwd 如上面的例子演示,切换目录后可以再用 pwd 命令确认一下当前目录是否正确 3) ls:list,语法格式为 $ ls [选项] [目录或文件名] 如果ls 命令后边是目录,会显示目录下包含的文件信息,如果是文件名则会显示该文件的信息,如果没有跟任何参数则显示当前工作目录下包含的文件信息。 常用选项: -a 显示所有文件,包括隐藏文件 -l 以长格式显示目录或文件的信息 -d 只显示目录本身的信息,不显示目录下包含的文件 -h human readable,用人性化显示的形式查看,比如以 K (KB),M (MB),G(GB)表示文件大小 4) mkdir:make directory,创建目录,语法格式为 $ mkdir [选项] 目录名 常用选项: -p 可以用于创建嵌套的多级目录 5) cp:copy,复制文件或目录 语法格式为 $ cp [选项] 源文件或目录 目标文件或目录 常用选项: -r 如果复制的是一个目录,则必须使用这个选项,会把目录下所有的内容都复制到目标目录中去 6) mv:move,移动文件或目录 语法格式为 $ mv [选项] 源文件或目录 目标文件或目录 如果移动的是一个目录,不需要加 -r 选项,可以直接将目录进行移动 7) rm:remove,删除文件或目录 语法格式为 $ rm [选项] 文件或目录 常用选项: -r 如果删除的是一个目录,则必须使用这个选项 -f 强制删除,无须用户确认 一般删除目录时,两个选项会同时使用 -rf,但是建议删除前先用 mv 命令将待删除的目录移动到一个指定的回收目录中去,等过一段时间确认不再需要这些文件和目录,再使用 rm 命令将其删除。 最后 本文介绍了 Linux 系统目录的结构和它们的用途,目录相关的概念,以及最常用到的相关命令。结合之前的文章《虚拟机安装 Linux 最完整攻略》,大家可以在自己的虚拟机进行演练,注意系统目录不要删除,否则系统可能就崩掉了,最好在自己的家目录或者临时目录中进行操作。 原文地址https://www.cnblogs.com/jfzhu/p/12940175.html
Python之路---初识函数 程序员三大美德: 懒惰因为一直致力于减少工作的总工作量。 缺乏耐性因为一旦让你去做本该计算机完成的事,你将会怒不可遏。 傲慢因为被荣誉感冲晕头的你会把程序写得让谁都挑不出毛病来。 大家好, 现在让我们一起来学习一下函数 1|1Why-为什么要使用函数? 现在我们需要计算一个字符串的长度,我们可以直接使用len()方法: num = len('hello') 但是如果我们不用len方法,怎么实现这个需求?其实也不难: s = 'hello' length = 0 for i in s: length += 1 print(length) 好了,功能实现了,然后又有一个需求(产品经理日常加需求),要计算另一个字符串的长度---“world”。 于是,本着程序员三大美德,我们使用最强大的快捷键---Ctrl+c , Ctrl+v: s1 = 'world' length = 0 for i in s1: length += 1 print(length) 这样确实可以实现需求,但是总感觉不是那么完美,为什么呢? 首先,之前我只需要执行len方法就可以拿到一个字符串的长度,而现在为了实现相同功能,我们需要把相同的代码写很多遍 --- 代码冗余 其次,刚刚的代码不是那么容易读懂 ---- 可读性差 我们就想,要是我们能像使用len一样使用我们这一大段“计算长度”的代码就好了。这种感觉有点像给这段代码起了一个名字,等我们用到的时候直接喊名字就能执行这段代码就行。 1|2函数定义与调用 现在教大家一个技能,把代码装起来: def mylen(): """计算字符串长度""" s = 'hello' length = 0 for i in s: length += 1 print(length) 来分析下这段代码: 其实除了def这一行和后面的缩进,其它的和前面的代码是一样的。 现在执行以下,你会发现啥也没发生! 因为这里我们只是把代码装起来了,还不会往外拿,拿应该怎么拿出来呢? mylen() 这就是代码取出来的过程。 定义函数 def mylen(): """计算字符串长度""" s = 'hello' length = 0 for i in s: length += 1 print(length) # 调用函数 mylen() 总结: 定义: def关键字开头,空格后接函数名称和圆括号(),最后还有一个“:”函数名只能包含字符串、下划线和数字且不能以数字开头。虽然函数名可以随便起,但我们给函数起名字还是要尽量简短,并能表达函数功能括号:是必须加的,先别问为啥要有括号,总之加上括号就对了!注释:每一个函数都应该对功能和参数进行相应的说明,应该写在函数下面第一行。以增强代码的可读性。调用: 就是 函数名() 要记得加上括号 1|3函数返回值 前面我们写了一个函数,这个函数可以帮助我们计算字符串的长度,并且把结果打印出来。但是,这和我们的len方法还不是太一样。以前我们调用len方法会得到一个值,我们可以用一个变量来接收这个值。 str_len = len('hello') 那么我们写的函数能做到吗? 定义函数 def mylen(): """计算字符串长度""" s = 'hello' length = 0 for i in s: length += 1 print(length) # 调用函数 str_len = mylen() print('str_len : %s'%str_len) 执行代码,获得的输出结果是str_len值为None,这说明什么也没返回 那么应该怎么让他有返回值呢? return 定义函数 def mylen(): """计算字符串长度""" s = 'hello' length = 0 for i in s: length += 1 return length # 调用函数 str_len = mylen() print('str_len : %s'%str_len) 只需要在函数最后加上一个return的关键字,return后面写上你要返回的值就可以了 return关键字的作用 return 是一个关键字,在pycharm里,你会看到它变成蓝色了。 这个词翻译过来就是“返回”,所以我们管写在return后面的值叫“返回值” 没有返回值:不写return的情况下,会默认返回一个None只写return,后面不写其他内容,也会返回None,那么为啥要写呢?return的其他用法,就是一旦遇到return,结束整个函数。return None:和上面的两种情况一样,我们一般不这样写。返回一个值: 注意:*return和返回值之间要有空格,可以返回任意数据类型的值 返回多个值:可以返回任意多个、任意数据类型的值 def demo1(): '''返回多个值''' return 1,2,3,4 def demo2(): '''返回多个任意类型的值''' return 1,['a','b'],3,4 ret1 = demo1() print(ret1) ret2 = demo2() print(ret2) 返回的多个值会被组织成元组被返回,也可以用多个值来接收 def demo3(): return 1,['a','b'],3,4 #返回多个值,用一个变量接收 ret3 = demo3() print(ret2) #返回多个值,用多个变量接收 a,b,c,d = demo3() print(a,b,c,d) #用多个值接收返回值:返回几个值,就用几个变量接收 a,b,c,d = demo3() print(a,b,c,d) 1|4函数的参数 这个函数还是不完美,因为之前我们使用len函数的时候,是可以想计算谁就计算谁的长度,但是我们写的这个函数只能计算“hello”的长度,换一个字符串就需要更改函数内部的变量,这样可不行,那应该怎么办? 带参数的函数: 函数定义 def mylen(s): """计算s1的长度""" length = 0 for i in s: length = length + 1 return length #函数调用 str_len = mylen("hello") print('str_len : %s'%str_len) 我们告诉mylen函数要计算的字符串是谁,这个过程叫做传递参数,简称传参,调用函数 时传递的这个“hello”和定义函数的s就是参数。 实参和形参我们调用函数时传递的这个“hello”被称为实际参数,因为这个是实际的要交给函数的内容,简称实参。 定义函数时的s,只是一个变量的名字,被称为形式参数,因为在定义函数的时候它只是一个形式,表示这里有一个参数,简称形参。 传递多个参数参数可以传递多个,多个参数之间用逗号分割。 def mymax(x,y): """比较两个数的大小""" the_max = x if x > y else y return the_max the_max = mymax(9,99) print(the_max) 正是因为需要传递多个参数,所以才有了下面的参数类型 1.位置参数按照位置传值: def mymax(x,y): """比较两个数的大小""" the_max = x if x > y else y return the_max the_max = mymax(9,99) print(the_max) 2.关键字参数按照关键字传值: def mymax(x,y): """比较两个数的大小""" the_max = x if x > y else y return the_max the_max = mymax(y=99, x=9) print(the_max) PS:位置、关键字形式混着用def mymax(x,y): """比较两个数的大小""" the_max = x if x > y else y return the_max the_max = mymax(9, y=99) print(the_max) 正确用法: 位置参数必须在关键字参数的前面 对于一个形参只能赋值一次 默认参数why?将变化比较小的值设置成默认参数 def info(name, sex='男'): """打印学生信息""" print(name, sex) info('马克') info('王富贵', '女') 参数陷阱:默认参数是一个可变数据类型 def func(a, l=[]): l.append(a) print(l) func(1) func(2) func(3) 动态参数(不定长参数)*args:按位置传值多余的参数都由args统一接收,保存成一个元组的形式 def mysum(*args): """求和""" the_sum = 0 print(args) # 保存成元组形式 print(type(args)) for i in args: the_sum += i return the_sum sum = mysum(10, 20, 30) print(sum) **kwargs:按位置传值多余的参数都由kwargs统一接收,保存成一个字典的形式 def stu_info(**kwargs): print(kwargs) # 保存成字典 print(kwargs['name']) stu_info(name='不喜欢马赛克的马克', sex='男') 1|5⭐️总结 函数: 参数: EOF 本文作者:Mark本文链接:https://www.cnblogs.com/mark-wq/p/12933407.html
都0202年了,你还不知道javascript有几种继承方式? 前言 当面试官问你:你了解js哪些继承方式?es6的class继承是如何实现的?你心中有很清晰的答案吗?如果没有的话,可以通过阅读本文,帮助你更深刻地理解js的所有继承方式。 js继承总共分成5种,包括构造函数式继承、原型链式继承、组合式继承、寄生式继承和寄生组合式继承。 构造函数式继承 首先来看第一种,构造函数式继承,顾名思义,也就是利用函数去实现继承; 假设我们现在有一个父类函数: // 父类构造函数function Parent(color) { this.color = color; this.print = function() { console.log(this.color); } } 现在要编写一个子类函数来继承这个父类,如下: // 子类构造函数function Son(color) { Parent.call(this, color); }上面代码可以看到,子类Son是通过Parent.call的方式去调用父类构造函数,然后把this对象传进去,执行父类构造函数之后,子类Son就拥有了父类定义的color和print方法。 调用一下该方法,输出如下: // 测试var son1 = new Son('red');son1.print(); // redvar son2 = new Son('blue');son2.print(); // blue 可以看到son1和son2都正常继承了父类的print方法和各自传进去的color属性; 以上就是构造函数式继承的实现了,这是最原始的js实现继承的方式; 但是当我们深入想一下会发现,这种根本就不是传统意义上的继承! 因为每一个Son子类调用父类生成的对象,都是各自独立的,也就是说,如果父类希望有一个公共的属性是所有子类实例共享的话,是没办法实现的。什么意思呢,来看下面的代码: function Flower() { this.colors = ['黄色', '红色']; this.print = function () { console.log(this.colors) } }function Rose() { Flower.call(this); }var r1 = new Rose();var r2 = new Rose();console.log(r1.print()); // [ '黄色', '红色' ]console.log(r2.print()); // [ '黄色', '红色' ] 我们现在有一个基类Flower,它有一个属性colors,现在我们把某一个实例的colors值改一下: r1.colors.push('紫色');console.log(r1.print()); // [ '黄色', '红色', '紫色' ]console.log(r2.print()); // [ '黄色', '红色' ] 结果如上,显然,改变的只有r1的值,因为通过构造函数创造出来的实例对象中,所有的属性和方法都是实例内部独立的,并不会跟其他实例共享。 总结一下构造函数的优缺点:优点:所有的基本属性独立,不会被其他实例所影响;缺点:所有希望共享的方法和属性也独立了,没有办法通过修改父类某一处来达到所有子实例同时更新的效果;同时,每次创建子类都会调用父类构造函数一次,所以每个子实例都拷贝了一份父类函数的内容,如果父类很大的话会影响性能;原型链继承 下面我们来看第二种继承方式,原型链式继承; 同样先来看下例子: function Parent() { this.color = 'red'; this.print = function() { console.log(this.color); } }function Son() {} 我们有一个父类和一个空的子类; Son.prototype = new Parent();Son.prototype.constructor = Son; 接着我们把子函数的原型属性赋值给了父函数的实例; var son1 = new Son();son1.print(); // red 最后新建子类实例,调用父类的方法,成功拿到父类的color和print属性方法; 我们重点来分析一下下面两行代码:Son.prototype = new Parent();Son.prototype.constructor = Son; 这段代码中,子函数的原型赋给了父函数的实例,我们知道prototype是函数中的一个属性,js的一个特性就是:如果一个对象某个属性找不到,会沿着它的原型往上去寻找,直到原型链的最后才会停止寻找。 关于原型更多基础的知识,可以参考一下其他文章,或许以后我也会出一期专门讲解原型和原型链的文章。 回到代码,我们看到最后实例son成功调用了Print方法,输出了color属性,这是因为son从函数Son的prototype属性上面去找到的,也就是从new Parent这个对象里面找到的; 这种方式也不是真正的继承,因为所有的子实例的属性和方法,都在父类同一个实例上了,所以一旦某一个子实例修改了其中的方法,其他所有的子实例都会被影响,来看下代码: function Flower() { this.colors = ['黄色', '红色']; this.print = function () { console.log(this.colors) } }function Rose() {}Rose.prototype = new Flower();Rose.prototype.constructor = Rose;var r1 = new Rose();var r2 = new Rose();console.log(r1.print()); // [ '黄色', '红色' ]console.log(r1.print()); // [ '黄色', '红色' ]r1.colors.push('紫色');console.log(r1.print()); // [ '黄色', '红色', '紫色' ]console.log(r2.print()); // [ '黄色', '红色', '紫色' ] 还是刚才的例子,这次Rose子类选择了原型链继承,所以,子实例r1修改了colors之后,r2实例的colors也被改动了,这就是原型链继承不好的地方。 来总结下原型链继承的优缺点:优点:很好的实现了方法的共享;缺点:正是因为什么都共享了,所以导致一切的属性都是共享的,只要某一个实例进行修改,那么所有的属性都会变化组合式继承 这里来介绍第三种继承方式,组合式继承; 这种继承方式很好理解,既然构造函数式继承和原型链继承都有各自的优缺点,那么我们把它们各自的优点整合起来,不就完美了吗? 组合式继承做的就是这个事情~来看一段代码例子: function Parent(color) { this.color = color; }Parent.prototype.print = function() { console.log(this.color); }function Son(color) { Parent.call(this, color); }Son.prototype = new Parent();Son.prototype.constructor = Son;var son1 = new Son('red');son1.print(); // redvar son2 = new Son('blue');son2.print(); // blue 上面代码中,在Son子类中,使用了Parent.call来调用父类构造函数,同时又将Son.prototype赋给了父类实例;为什么要这样做呢?为什么这样就能解决上面两种继承的问题呢? 我们接着分析一下,使用Parent.call调用了父类构造函数之后,那么,以后所有通过new Son创建出来的实例,就单独拷贝了一份父类构造函数里面定义的属性和方法,这是前面构造函数继承所提到的一样的原理; 然后,再把子类原型prototype赋值给父类的实例,这样,所有子类的实例对象就可以共享父类原型上定义的所有属性和方法。这也不难理解,因为子实例会沿着原型链去找到父类函数的原型。 因此,只要我们定义父类函数的时候,将私有属性和方法放在构造函数里面,将共享属性和方法放在原型上,就能让子类使用了。 以上就是组合式继承,它很好的融合了构造函数继承和原型链继承,发挥两者的优势之处,因此,它算是真正意义上的继承方式。 寄生式继承 既然上面的组合式继承都已经这么完美了,为什么还需要其他的继承方式呢? 我们细想一下,Son.prototype = new Parent();这行代码,它有什么问题没有? 显然,每次我们实例化子类的时候,都需要调用一次父类构造函数,那么,如果父类构造函数是一个很大很长的函数,那么每次实例化子类就会执行很长时间。 实际上我们并不需要重新执行父类函数,我们只是想要继承父类的原型。 寄生式继承就是在做这个事情,它是基于原型链式继承的改良版: var obj = { color: 'red', print: function() { console.log(this.color); } };var son1 = Object.create(obj);son1.print(); // redvar son2 = Object.create(obj);son2.print(); // red 寄生式继承本质上还是原型链继承,Object.create(obj);方法意思是以obj为原型构造对象,所以寄生式继承不需要构造函数,但是同样有着原型链继承的优缺点,也就是它把所有的属性和方法都共享了。 寄生组合式继承 接下来到我们最后一个继承方式,也就是目前业界最为完美的继承解决方案:寄生组合式继承。 没错,它就是es6的class语法实现原理。 但是如果你理解了组合式继承,那么理解这个方式也很简单,只要记住,它出现的主要目的,是为了解决组合式继承中每次都需要new Parent导致的执行多一次父类构造函数的缺点。 下面来看代码: function Parent(color) { this.color = color; }Parent.prototype.print = function() { console.log(this.color); }function Son(color) { Parent.call(this, color); }Son.prototype = Object.create(Parent.prototype);Son.prototype.constructor = Son;var son1 = new Son('red');son1.print(); // redvar son2 = new Son('blue');son2.print(); // blue 这段代码不同之处只有一个,就是把原来的Son.prototype = new Parent();修改为了Son.prototype = Object.create(Parent.prototype); 我们前面讲过,Object.create方法是以传入的对象为原型,创建一个新对象;创建了这个新对象之后,又赋值给了Son.prototype,因此Son的原型最终指向的其实就是父类的原型对象,和new Parent是一样的效果; 到这里,我们5中js的继承方式也就讲完了; 如果你对上面的内容感到疑问或者不理解的,可以留言和我交流,或者关注公众号直接联系我~ 最后感谢小伙伴的阅读,如果觉得文章写的还可以的话,欢迎点个赞、点个关注,我会持续输出优质的技术分享文章。原文地址https://www.cnblogs.com/oujiamin/p/12924625.html
Spring处理@Configuration的分析 声明:本文若有任何纰漏、错误,还请不吝指出! 序言#@Configuration注解在SpringBoot中作用很大,且不说SpringBoot中的外部化配置,一些第三方组件也是通过这个注解完成整合的,常用的比如说mybatis,就是利用了@Configuration这个注解来实现的。 在注解类中,还可以使用@Bean的方式向Spring容器中,注入一些我们自定义的组件。 在SpringBoot中各种Enable又是如何实现的?和@Configuration又有什么联系呢? 这就要了解Spring是怎么对待被@Configuration所注解的类。 环境#SpringBoot 2.2.6RELEASE Spring 5.2.5.RELEASE 正文#注解依附于具体的Java类,所以如果想获取注解的信息,必须先将类加载进来,才能从Class对象获取到其注解元信息。 好在Spring容器启动之前,已经把所有需要加载的Bean,封装成一个BeanDefinition对象,最终注册到BeanDefinitionRegistry中。 BeanDefinition包含了一个Bean所有的信息,自然也包含了它的元注解信息。 有了这个就能轻而易举的获取到标注有@Configuration注解的BeanDefinition,从而去处理这个配置类拥有的各种配置信息。 有了BeanDefinition之后,下面一步就是要进行Bean的实例化了。如果一个Bean被实例化后,就没有可操作的机会了,因此Spring在Bean的实例化前预留了一些自定义的处理时机。 BeanFactoryPostProcessor就是这样的一个功能,用于在Bean实例化之前,做一些其他的处理操作。 对配置类的处理,也正是利用了这一预留点。 BeanDefinitionRegistryPostProcessor#处理配置类,第一步就要从茫茫的BeanDefinition中,找出哪些是配置类。 容器开始启动之前的一些准备动作,这里不说明,主要是扫描classpath,然后将生成BeanDefinition。 直接从容器的启动开始简单下调用栈 Spring容器真正开始启动的是从这里开始的org.springframework.context.support.AbstractApplicationContext#refresh,在这个方法中,会去执行所有的BeanFactoryPostProcessor。 通过一个委托类org.springframework.context.support.PostProcessorRegistrationDelegate,执行所有的BeanFactoryPostProcessor后置处理逻辑。 BeanFactoryPostProcessor有一个子接口是BeanDefinitionRegistryPostProcessor,这个接口的主要作用就是在其他后置处理执行之前,额外注册一些BeanDefinition进来。 想想在配置类中使用的@Import和@Bean,就可以猜到,这些注解的处理就是由这个处理器进行处理的。 BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor是放到一起处理的,只不过BeanDefinitionRegistryPostProcessor的执行时机,早于BeanFactoryPostProcessor的执行时机。 Copy// org.springframework.context.support.PostProcessorRegistrationDelegatepublic static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) { Set processedBeans = new HashSet<>(); // 如果BeanFactory同时又是一个BeanDefinitionRegistry的话 // 例如 DefaultListaleBeanFactory if (beanFactory instanceof BeanDefinitionRegistry) { BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>(); List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>(); // 如果有直接注册到Context的后置处理器, // 先执行直接添加到ApplicationContext的BeanDefinitionRegistryPostProcessor处理器 for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { BeanDefinitionRegistryPostProcessor registryProcessor = (BeanDefinitionRegistryPostProcessor) postProcessor; // 执行BeanDefinitionRegistryPostProcessor处理器 registryProcessor.postProcessBeanDefinitionRegistry(registry); // BeanDefinitionRegistryPostProcessor同时又是一个BeanFactoryPostProcessor // 待所有的BeanDefinitionRegistryPostProcessor执行完后,再来执行它 registryProcessors.add(registryProcessor); } else { // 加入到BeanFactoryPostProcessor处理器集合中,待所有的BeanDefinitionRegistryPostProcessor执行完后,来执行它 regularPostProcessors.add(postProcessor); } } List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>(); // 对从BeanDefinitionRegistry中的BeanDefinition做后置处理 // 先执行被@PriorityOrdered注解的BeanDefinitionRegistryPostProcessor // 并且按排序大小进行优先级排序 // 根据类型,从BeanDefinitionRegistry中查找出所有的BeanDefinitionRegistryPostProcessor的是实现类,及子类 String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { // 使用@PriorityOrdered注解的先查找出来 if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } // 按编号大小排序,升序排列 sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); // 执行BeanDefinitionRegistryPostProcessor invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); currentRegistryProcessors.clear(); // 处理被注解@Ordered标注的BeanDefinitionRegistryPostProcessor, // 并且按排序后排序后执行 // 根据类型,从BeanDefinitionRegistry中查找出所有的BeanDefinitionRegistryPostProcessor的是实现类,及子类 postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { // 没被处理过且被注解@Ordered if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); // 执行BeanDefinitionRegistryPostProcessor invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); currentRegistryProcessors.clear(); // 再去执行其他的剩下的所有BeanDefinitionRegistryPostProcessor boolean reiterate = true; while (reiterate) { reiterate = false; postProcessorNames= beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { if (!processedBeans.contains(ppName)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); reiterate = true; } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); currentRegistryProcessors.clear(); } // BeanDefinitionRegistryPostProcessor也是一个BeanFactoryPostProcessor // 下面这部分就是执行postProcessBeanFactory方法, // 会在@Configuration的proxyBeanMethods为true时对配置类做一个CGLIB增强, // 表示对配置类中的BeanMethod创建时,使用代理创建 // 将增强后的类,替换到其BeanDefinition#setBeanClass invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); // 最后再执行直接注册到到ApplicationContext中的BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); } else { // 处理直接通过ApplicationContext实例注册的BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory); } // 上面就执行过了定义的所有的BeanDefinitionRegistryPostProcessor,以及实现的 // BeanFactoryPostProcessor#postProcessBeanFactory方法 // 接下来回去执行所有的BeanFactoryPostProcessor处理器 // 查找出所有注册的类型为BeanFactoryPostProcessor的BeanDefinition的name数组 String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); // 分别归类出使用@PriorityOrdered 和 @Ordered注解和没有使用的 List priorityOrderedPostProcessors = new ArrayList<>(); List orderedPostProcessorNames = new ArrayList<>(); List nonOrderedPostProcessorNames = new ArrayList<>(); for (String ppName : postProcessorNames) { if (processedBeans.contains(ppName)) { // 处理过的,不用重复处理 } else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); } else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { orderedPostProcessorNames.add(ppName); } else { nonOrderedPostProcessorNames.add(ppName); } } // 优先处理 PriorityOrdered. sortPostProcessors(priorityOrderedPostProcessors, beanFactory); invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); // 其次 Ordered. List orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size()); for (String postProcessorName : orderedPostProcessorNames) { orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); } sortPostProcessors(orderedPostProcessors, beanFactory); invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); //最后普通的 BeanFactoryPostProcessor List nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size()); for (String postProcessorName : nonOrderedPostProcessorNames) { nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); } invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory); beanFactory.clearMetadataCache();}上面这个方法执行完后,已经完成了所有BeanFactoryPostProcessor的执行,也自然已经处理过所有的配置类了。 ConfigurationClassPostProcessor#在众多的后置处理器中,有一个独属于@Configuration的后置处理器,就是ConfigurationClassPostProcessor,一个好的命名的效果,就体现出来了。 下面这个方法,负责两件事 从BeanDefinitionRegistry中筛选出配置类对配置类的BeanDefinition进行解析Copy// org.springframework.context.annotation.ConfigurationClassPostProcessorpublic void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { // 候选配置类集合 List configCandidates = new ArrayList<>(); // 获取所有的BeanDefinition的name数组 String[] candidateNames = registry.getBeanDefinitionNames(); for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); // 如果BeanDefinition中有这个属性存在,说明作为一个配置类已经被处理过了 if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { if (logger.isDebugEnabled()) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } } // 检查是否为一个配置类 // 查看是否具有@Configuration注解 // 这里不会仅仅看BeanDefinition所代表的类直接标注的注解,而是会递归查找其注解的注解是否有为 // @Configuration,只要找到了那么当前的类就是一个配置类 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // 找不到就结束 if (configCandidates.isEmpty()) { return; } // 对使用了@Order的进行排序 自然排序也就是升序 // 注意不是@Ordered configCandidates.sort((bd1, bd2) -> { int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); return Integer.compare(i1, i2); }); // 如果有自定义Bean Name生成器,就使用自定义的 SingletonBeanRegistry sbr = null; if (registry instanceof SingletonBeanRegistry) { sbr = (SingletonBeanRegistry) registry; if (!this.localBeanNameGeneratorSet) { BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); if (generator != null) { this.componentScanBeanNameGenerator = generator; this.importBeanNameGenerator = generator; } } } // 如果还没有初始化Environment对象,初始化一个 if (this.environment == null) { this.environment = new StandardEnvironment(); } // 解析每一个被@Configuratin标注的注解类 ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set candidates = new LinkedHashSet<>(configCandidates); Set alreadyParsed = new HashSet<>(configCandidates.size()); do { parser.parse(candidates); parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); configClasses.removeAll(alreadyParsed); // 构造一个BeanDefinitionReader if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } // 加载配置类中的@Bean,生成BeanDefinition this.reader.loadBeanDefinitions(configClasses); alreadyParsed.addAll(configClasses); candidates.clear(); // 下面这段主要是考虑到@Import进来的或者@ImportSource或者@Bean等方式注入进来的会有配置类 if (registry.getBeanDefinitionCount() > candidateNames.length) { String[] newCandidateNames = registry.getBeanDefinitionNames(); Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames)); Set<String> alreadyParsedClasses = new HashSet<>(); for (ConfigurationClass configurationClass : alreadyParsed) { alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } for (String candidateName : newCandidateNames) { if (!oldCandidateNames.contains(candidateName)) { BeanDefinition bd = registry.getBeanDefinition(candidateName); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) { candidates.add(new BeanDefinitionHolder(bd, candidateName)); } } } candidateNames = newCandidateNames; } } while (!candidates.isEmpty()); //把 ImportRegistry注册成一个Bean,以便支持 继承ImportAware 有注解类@Configuration的配置类 if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } }这个方法执行完后,所有的配置类都会被进行处理,并且在此过程中,BeanDefinition的总量有可能会增加,有新的BeanDefinition在解析过程新增进来。 这些BeanDefinition的来源就是存在于配置类上的其他注解 ConfigurationClassParser#SpringBoot是如何使用一个@SpringBootApplication注解,完成了那么多的事情? 答案就在下面揭晓 Copy// `org.springframework.context.annotation.ConfigurationClassParserprotected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { // 如果有Component注解 if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // 首先递归处理内部类 processMemberClasses(configClass, sourceClass, filter); } // 处理所有的@PropertySource注解 for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } // 处理所有的 @ComponentScan 和@ComponentScans Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // 继续检查扫描的BeanDefinition有没有是配置类的 for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { // 如果是的话,解析 parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } // 处理所有的@Import注解,将导入的Bean注册到BeanDefinitionRegistry // 这个也是会查找注解的注解,制止找到所有的@Import processImports(configClass, sourceClass, getImports(sourceClass), filter, true); // 处理所有的@ImportResource 注解,将导入的Bean注册到BeanDefinitionRegistry AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // 处理独立的 @Bean方法,生成BeanMethod // 使用@Bean,方法要是可重写,也就是不能为default/private,因为要使用CGLIB代理 // 详细可进去下面方法细看 Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // 处理接口的默认方法 processInterfaces(configClass, sourceClass); // 如果有父类,处理 if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } //没有父类,处理完成 return null; } 看下@SpringBootApplication的定义 Copy@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic @interface SpringBootConfiguration { } @Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { }可以看到@SpringBootApplication在功能上也是一个@Configuration。 这样就解释了,一般在SpringBoot的启动类上写了那么多注解,为啥可以被执行。 如果有看过各类@Enable注解,就一定会看到,每一个@Enable几乎都会被@Import所注解,而一般使用@Enable时,都会和@SpringBootApplication写一起,这个写法的一方面是比较清晰,集中写到一起,还有个原因就是部分@Enable在定义时,没有使用@Configuration来进行注解,需要借助于一个能被Spring容器启动时处理的配置类上。 上面的这段代码分析,正好解释了@Enable背后的实现原理。 总结#其实总的看下来,@Configuration就是一个标志注解,更大的作用就是为别的注解服务的。这么说有点矛盾,主要是觉得本身不具备什么功能性。 至于其能实现的对字段进行配置值绑定来说,可以使用@ConfigurationProperties或者@Value这两个注解来实现,由此可见,@Configuration并不是用于将配置文件的配置值,绑定到配置类的,这个工作和他没有任何关系,对于一些配置文件的配置来说,可以使用@Component注解来对普通的配置类注解,达到一样的效果,而并非一定要使用@Configuration(@Configuration注解派生自@Component)。 通过我们上面的分析,被@Configuration注解的类,仅有存在以上那几个注解时,才有意义,才能被ConfigurationClassPostProcessor所处理,而这个处理过程中,和配置值绑定一毛钱的关系都没有。 实际上配置值的绑定,都是在Bean实例化后,Bean属性填充期间进行的。 @ConfigurationProperties注解会在ConfigurationPropertiesBindingPostProcessor执行时进行处理,这个处理器是一个BeanPostProcessor。 @Value注解的处理是在AutowiredAnnotationBeanPostProcessor这个BeanPostProcessor中来处理的,这个处理器同时也是处理@Inject、 @Autowired、 @Resource的BeanPostProcesoor。 Spring或者SpringBoot中,大量的使用各种后置处理器,除了对主体框架(Bean的生命周期)的理解外,剩下的主要就是熟悉这些支持各种功能的PostProcessor。 还有个值得注意的是,@Configuration有个方法proxyBeanMethods,这个方法返回true时,默认也是true,会对我们的配置类,生成一个代理类,注意,这里是直接生成一个代理类,并且最后实例化时,也是使用这个代理类进行实例化Bean,这个就给我们一个启发,如果想对一些无法直接修改又被Spring容器所管理的的Bean,是否可以通过自定义BeanDefinitionRegistryProcessor的方式,来对原Class做一个增强,从而实现我们的目的。 PS:是否具备切实可行性,并不保证,只是觉得如果遇到,可以尝试下。 作者: 早知今日 出处:https://www.cnblogs.com/heartlake/p/12909362.html
c++离散化处理大范围和重复数据 关于离散化 有些新手可能会问:离散化是什么?离散化就是将无限空间中有限的个体映射到有限的空间里去。 上面的定义肯定会有人看不懂(其实我刚开始学的时候也看不懂) 用我自己的话来说,就是在不改变数据的相对大小的条件下,对数据进行相应的压缩 可能还是有人看不懂,没关系,我们来看一个例子,顺便来讲一下离散化的基本操作: 现有一个数组:1,100,2367,562,364737,19,1974832947,100,562,2367 如果按照正常的方法,该开1974832947的空间,但是经过离散化后,就不需要 那么step 1:排序 用上面的例子来说,就是将上面的数据排序并去重,得到下面这组数据: 1,19,100,100,562,562,2367,2367,364737,1974832947 然后step 2:通过unique去重使大小与下标对应,并得到去重后的长度,得到下面这组数据: 1,19,100,562,2367,364737,1974832947 接着step 3:通过lower_bound算出离散化后的排列,得到下面这组数据: 1,2,3,4,5,6,7 那么这里就很尴尬了,这组数据无法应用于初始数据 所以在开始,我们多定义1个数组,来记录初始情况下的数据,再用step 3与其进行对应。 最终得到答案:1,3,5,4,6,2,7,3,4,5 下面给出模板: View Code原文地址https://www.cnblogs.com/jasonownblog/p/12906712.html
Spring Cache的基本使用与分析 1|0概述使用 Spring Cache 可以极大的简化我们对数据的缓存,并且它封装了多种缓存,本文基于 redis 来说明。 2|0基本使用1、所需依赖 org.springframework.bootspring-boot-starter-data-redisorg.springframework.bootspring-boot-starter-cache 2、配置文件 spring: # redis连接信息 redis: host: 192.168.56.10 port: 6379 cache: # 指定使用的缓存类型 type: redis # 过期时间 redis: time-to-live: 3600000 # 是否开启前缀,默认为true use-key-prefix: true # 键的前缀,如果不配置,默认就是缓存名cacheNames key-prefix: CACHE_ # 是否缓存空置,防止缓存穿透,默认为true cache-null-values: true 3、Spring Cache 提供的注解如下,使用方法参见:官方文档,通过这些注解,我们可以方便的操作缓存数据。 @Cacheable:触发缓存写入的操作@CacheEvict:触发缓存删除的操作@CachePut:更新缓存,而不会影响方法的执行@Caching:重新组合要应用于一个方法的多个缓存操作,即对一个方法添加多个缓存操作@CacheConfig:在类级别共享一些与缓存有关的常见设置例如,如果需要对返回结果进行缓存,直接在方法上标注 @Cacheable 注解 @Cacheable(cacheNames = "userList") //指定缓存的名字,便于区分不同缓存 public List getUserList() { ... } 4、redis 默认使用 jdk 序列化,需要我们配置序列化机制,自定义一个配置类,否则存入的数据显示乱码 @EnableCaching //开启缓存 @Configuration public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration(){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); //指定键和值的序列化机制 config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return config; } } 5、使用以上配置后,虽然乱码的问题解决了,但配置文件又不生效了,比如过期时间等,这是因为在初始化时会判断用户是否自定义了配置文件,如果自定义了,原来的就不会生效,源码如下: private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader) { //如果配置了,就返回自定义的配置 if (this.redisCacheConfiguration != null) { return this.redisCacheConfiguration; } //没配置使用默认的配置 Redis redisProperties = this.cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration .defaultCacheConfig(); config = config.serializeValuesWith( SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } 6、所以,我们也需要手动获取 ttl、prefix 等属性,直接仿照源码就行,将配置类修改为如下: @EnableCaching //开启缓存 @Configuration @EnableConfigurationProperties(CacheProperties.class) //缓存的所有配置属性都在这个类里 public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { //获取默认配置 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); //指定键和值的序列化机制 config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); //获取配置文件的配置 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } } 3|0原理分析在 Spring 中 CacheManager 负责创建管理 Cache,Cache 负责缓存的读写,因此使用 redis 作为缓存对应的就有 RedisCacheManager 和 RedisCache。 打开 RedisCache 源码,我们需要注意这两个方法: 1、读取数据,未加锁 @Override protected Object lookup(Object key) { byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key)); if (value == null) { return null; } return deserializeCacheValue(value); } 2、读取数据,加锁,这是 RedisCache 中唯一一个同步方法 @Override public synchronized T get(Object key, Callable valueLoader) { ValueWrapper result = get(key); if (result != null) { return (T) result.get(); } T value = valueFromLoader(key, valueLoader); put(key, value); return value; } 通过打断点的方式可以知道 RedisCache 默认调用的是 lookup(),因此不能应对缓存穿透,如果有相关需求,可以这样配置:@Cacheable(sync = true),开启同步模式,此配置只在 @Cacheable 中才有。 4|0总结Spring Cache 对于读模式下缓存失效的解决方案: 缓存穿透:cache-null-values: true,允许写入空值缓存击穿:@Cacheable(sync = true),加锁缓存雪崩:time-to-live:xxx,设置不同的过期时间而对于写模式,Spring Cache 并没有相应处理,我们需要使用其它方式处理。 总的来说: 1、对于常规数据(读多写少,及时性、一致性要求不高的数据)完全可以使用 Spring Cache 2、对于特殊数据(比如要求高一致性)则需要特殊处理 EOF 本文作者:JLSong本文链接:https://www.cnblogs.com/songjilong/p/12901397.html
C++17结构化绑定 动机std::map的insert方法返回std::pair,两个元素分别是指向所插入键值对的迭代器与指示是否新插入元素的布尔值,而std::map::iterator解引用又得到键值对std::pair。在一个涉及std::map的算法中,有可能出现大量的first和second,让人不知所措。 include include int main(){ typedef std::map<int, int> Map; Map map; std::pair<Map::iterator, bool> result = map.insert(Map::value_type(1, 2)); if (result.second) std::cout << "inserted successfully" << std::endl; for (Map::iterator iter = map.begin(); iter != map.end(); ++iter) std::cout << "[" << iter->first << ", " << iter->second << "]" << std::endl; }C++11标准库添加了std::tie,用若干引用构造出一个std::tuple,对它赋以std::tuple对象可以给其中的引用一一赋值(二元std::tuple可以由std::pair构造或赋值)。std::ignore是一个占位符,所在位置的赋值被忽略。 include include include int main(){ std::map<int, int> map; bool inserted; std::tie(std::ignore, inserted) = map.insert({1, 2}); if (inserted) std::cout << "inserted successfully" << std::endl; for (auto&& kv : map) std::cout << "[" << kv.first << ", " << kv.second << "]" << std::endl; }但是这种方法仍远不完美,因为: 变量必须事先单独声明,其类型都需显式表示,无法自动推导;对于默认构造函数执行零初始化的类型,零初始化的过程是多余的;也许根本没有可用的默认构造函数,如std::ofstream。为此,C++17引入了结构化绑定(structured binding)。 include include int main(){ std::map<int, int> map; auto&& [iter, inserted] = map.insert({1, 2}); if (inserted) std::cout << "inserted successfully" << std::endl; for (auto&& [key, value] : map) std::cout << "[" << key << ", " << value << "]" << std::endl; }结构化绑定这一语言特性在提议的阶段曾被称为分解声明(decomposition declaration),后来又被改回结构化绑定。这个名字想强调的是,结构化绑定的意义重在绑定而非声明。 语法结构化绑定有三种语法: attr(optional) cv-auto ref-operator(optional) [ identifier-list ] = expression;attr(optional) cv-auto ref-operator(optional) [ identifier-list ] { expression };attr(optional) cv-auto ref-operator(optional) identifier-list ;其中,attr(optional)为可选的attributes,cv-auto为可能有const或volatile修饰的auto,ref-operator(optional)为可选的&或&&,identifier-list为逗号分隔的标识符,expression为单个表达式。 另外再定义initializer为= expression、{ expression }或( expression ),换言之上面三种语法有统一的形式attr(optional) cv-auto ref-operator(optional) [ identifier-list ] initializer;。 整个语句是一个结构化绑定声明,标识符也称为结构化绑定(structured bindings),不过两处“binding”的词性不同。 顺带一提,C++20中volatile的许多用法都被废弃了。 行为结构化绑定有三类行为,与上面的三种语法之间没有对应关系。 第一种情况,expression是数组,identifier-list的长度必须与数组长度相等。 第二种情况,对于expression的类型E,std::tuple_size是一个完整类型,则称E为类元组(tuple-like)类型。在STL中,std::array、std::pair和std::tuple都是这样的类型。此时,identifier-list的长度必须与std::tuple_size::value相等,每个标识符的类型都通过std::tuple_element推导出(具体见后文),用成员get()或get(e)初始化。显然,这些标准库设施是与语言核心绑定的。 第三种情况,E是非union类类型,绑定非静态数据成员。所有非静态数据成员都必须是public访问属性,全部在E中,或全部在E的一个基类中(即不能分散在多个类中)。identifier-list按照类中非静态数据成员的声明顺序绑定,数量相等。 应用结构化绑定擅长处理纯数据类型,包括自定义类型与std::tuple等,给实例的每一个字段分配一个变量名: include struct Point{ double x, y; }; Point midpoint(const Point& p1, const Point& p2){ return { (p1.x + p2.x) / 2, (p1.y + p2.y) / 2 }; } int main(){ Point p1{ 1, 2 }; Point p2{ 3, 4 }; auto [x, y] = midpoint(p1, p2); std::cout << "(" << x << ", " << y << ")" << std::endl; }配合其他语法糖,现代C++代码可以很优雅: include include int main(){ std::map<int, int> map; if (auto&& [iter, inserted] = map.insert({ 1, 2 }); inserted) std::cout << "inserted successfully" << std::endl; for (auto&& [key, value] : map) std::cout << "[" << key << ", " << value << "]" << std::endl; }利用结构化绑定在类元组类型上的行为,我们可以改变数据类型的结构化绑定细节,包括类型转换、是否拷贝等: include include include class Transcript { / ... / }; class Student{public: const char* name; Transcript score; std::string getName() const { return name; } const Transcript& getScore() const { return score; } template<std::size_t I> decltype(auto) get() const { if constexpr (I == 0) return getName(); else if constexpr (I == 1) return getScore(); else static_assert(I < 2); } }; namespace std{template<>struct tuple_size : std::integral_constant<std::size_t, 2> { }; template<>struct tuple_element<0, Student> { using type = decltype(std::declval().getName()); }; template<>struct tuple_element<1, Student> { using type = decltype(std::declval().getScore()); };} int main(){ std::cout << std::boolalpha; Student s{ "Jerry", {} }; const auto& [name, score] = s; std::cout << name << std::endl; std::cout << (&score == &s.score) << std::endl; }Student是一个数据类型,有两个字段name和score。name是一个C风格字符串,它大概是从C代码继承来的,我希望客户能用上C++风格的std::string;score属于Transcript类型,表示学生的成绩单,这个结构比较大,我希望能传递const引用以避免不必要的拷贝。为此,我写明了三要素:std::tuple_size、std::tuple_element和get。这种机制给了结构化绑定很强的灵活性。 细节 include include include int main(){ std::pair pair{ 1, 2.0 }; int number = 3; std::tuple<int&> tuple(number); const auto& [i, f] = pair; //i = 4; // error const auto& [ri] = tuple; ri = 5; }如果结构化绑定i被声明为const auto&,对应的类型为int,那么它应该是个const int&吧?i = 4;出错了,看起来正是如此。但是如何解释ri = 5;是合法的呢? 这个问题需要系统地从头谈起。先引入一个名字e,E为其类型: 当expression是数组类型A,且ref-operator不存在时,E为cv A,每个元素由expression中的对应元素拷贝(= expression)或直接初始化({ expression }或( expression );否则,相当于定义e为attr cv-auto ref-operator e initializer;。也就是说,方括号前面的修饰符都是作用于e的,而不是那些新声明的变量。至于为什么第一条会独立出来,这是因为在标准C++中第二条的形式不能用于数组拷贝。 然后分三种情况讨论: 数组情形,每个结构化绑定都是指向e数组中元素的左值(但不是左值引用)——int array[2]{ 1, 2 }; auto& [i, j] = array; static_assert(!std::is_reference_v);;类元组情形,如果e是左值引用,则e是左值(lvalue),否则是消亡值(xvalue);记Ti为std::tuple_element::type,则结构化绑定vi的类型是Ti的引用;当get返回左值引用时是左值引用,否则是右值引用;数据成员情形,与数组类似,设数据成员mi被声明为Ti类型,则结构化绑定的类型是指向cv Ti的左值(同样不是左值引用)。至此,我想“结构化绑定”的意义已经明确了:标识符总是绑定一个对象,该对象是另一个对象的成员(或数组元素),后者或是拷贝或是引用(引用不是对象,意会即可)。与引用类似,结构化绑定都是既有对象的别名(这个对象可能是隐式的);与引用不同,结构化绑定不一定是引用类型。 (不理解的话可以参考N4659 11.5节,尽管你很可能会更加看不懂……) 现在可以解释ri非const的现象了:编译器先创建了变量const auto& e = tuple;,E为const std::tuple&,std::tuple_element<0, E>::type为int&,std::get<0>(e)同样返回int&,故ri为int&类型。 在面向底层的C++编程中常用union和位域(bit field),结构化绑定支持这样的数据成员。如果类有union类型成员,它必须是命名的,绑定的标识符的类型为该union类型的左值;如果有未命名的union成员,则这个类不能用于结构化绑定。 C++中不存在位域的指针和引用,但结构化绑定可以是指向位域的左值: include struct BitField{ int f1 : 4; int f2 : 4; int f3 : 4; }; int main(){ BitField b{ 1, 2, 3 }; auto& [f1, f2, f3] = b; f2 = 4; auto print = [&] { std::cout << b.f1 << " " << b.f2 << " " << b.f3 << std::endl; }; print(); f2 = 21; print(); }程序输出: 1 4 31 5 3f2的功能就像位域的引用一样,既能写回原值,又不会超出位域的范围。 还有一些语法细节,比如get的名字查找、std::tuple_size没有value、explicit拷贝构造函数等,除非是深挖语法的language lawyer,在实际开发中不必纠结(上面这一堆已经可以算language lawyer了吧)。 局限以上代码示例应该已经囊括了所有类型的结构化绑定应用,你能想象到的其他语法都是错的,包括但不限于: 用std::initializer_list初始化;因为std::initializer_list的长度是动态的,但结构化绑定的标识符数量是静态的。 用列表初始化——auto [x,y,z] = {1, "xyzzy"s, 3.14159};;这相当于声明了三个变量,但结构化绑定的意图在于绑定而非声明。 不声明而直接绑定——[iter, success] = mymap.insert(value);;这相当于用std::tie,所以请继续用std::tie。另外,由[开始可能与attributes混淆,给编译器和编译器设计者带来压力。 指明结构化绑定的修饰符——auto [& x, const y, const& z] = f();;同样是脱离了结构化绑定的意图。如果需要这样的功能,或者一个个定义变量,或者手动写上三要素。 指明结构化绑定的类型——SomeClass [x, y] = f();或auto [x, std::string y] = f();;第一种可用auto [x, y] = SomeClass{ f() };代替;第二种同上一条。 显式忽略一个结构化绑定——auto [x, std::ignore, z] = f();;消除编译器警告是一个理由,但是auto [x, y, z] = f(); (void)y;亦可。这还涉及一些语言问题,请移步P0144R2 3.8节。 标识符嵌套——std::tuple, T4> f(); auto [ w, [x, y], z ] = f();;多写一行吧。[同样可能与attributes混淆。 以上语法都没有纳入C++20标准,不过可能在将来成为C++语法的扩展。 延伸C++17的新特性不是孤立的,与结构化绑定相关的有: 类模板参数推导(class template argument deduction,CTAD),由构造函数参数推导类模板参数;拷贝消除,保证NRV(named return value)优化;constexpr if,简化泛型代码,消除部分SFINAE;带初始化的条件分支语句:语法糖,使代码更加优雅。原文地址https://www.cnblogs.com/jerry-fuyi/p/12892288.html
C# 数据操作系列 - 5. EF Core 入门 0.前言上一章简单介绍了一下ORM框架,并手写了一个类似ORM的工具类。这一章将介绍一个在C#世界里大名鼎鼎的ORM框架——Entity Framework的Core版。 Entity Framework 非Core版目前已经更新到了6代,这是一款经过检验的ORM框架。在这里简单介绍一下Entity Framework(简称EF,额,别拿这个当关键字搜索,要不然你会被忽悠到一个英语培训机构的)的优点。 C#的设计理念是约定优于配置,意思就是通过一定程度的规范性格式化的写法来避免使用配置文件或者配置代码等。而EF可以说是很好的诠释了这个理念。 EF可以在不使用任何配置的前提下,自动解析类与表之间的映射(具体的映射逻辑与我们手写的ORM工具类一致或相近)。自动跟踪更改。在直接使用通过EF获取的元素时,EF会自动跟踪哪些字段发生了变化,当手动调用保存的时候,EF就会把数据回传给数据库。可以延迟加载需要的数据,外键引用属性、查询结果等丰富的映射关系,支持一对一,一对多,多对多,甚至继承、单表多实例等可以使用Linq 进行查询非Core版的可以通过数据库表生成实体类,两种都可以通过实体类生成表基于 ADO.NET 的数据库连接和可用于连接到 SQL Server、Oracle、MySQL、SQLite、PostgreSQL、DB2 等当然,还有一个特点:EF是约定优于配置,所以EF也可以配置。EF可以使用Fluent式配置,也可以使用配置文件进行配置。 说了一大堆Entity Framework的优点,那么就让我们开始使用Entity Framework Core吧。 这里简单介绍一下选择Core的原因,微乳这几年一直在主推跨平台战略。因为EF更多的是基于.NET Framework开发的,所以微软以EF为基础针对.net core做了一定的修改,然后EF Core诞生了。可以说EF Core是专门为.net core开发的。而且.net core有更多更好的发展。 Entity Framework Core安装现在就让我们一起来试着用一下EntityFramework Core吧。 先新建一个项目: Visual Studio 点下一步,选择Console程序: 点击创建 Visual Studio Codedotnet new console -o ef_democd ef_demo然后用VS Code打开 ef_demo目录。 然后选择数据库: 这次与之前的选择不太一样,这次选择 SQLite这个数据库。这是一个超小型的数据库,可以不用安装任何附加软件,只要有一个文件,然后通过代码就可以访问了。 接下来,添加 EF的SQLite包: 在非Visual Studio环境下,安装一个三方库可以使用: dotnet add package Microsoft.EntityFrameworkCore.Sqlite这个命令进行安装。这是dotnet命令行安装三方包的命令。对于Visual Studio或者Rider都可以通过图形化的NuGet安装三方包。 如果是使用NuGet的命令行界面进行安装的话,可以通过: Install-Package Microsoft.EntityFrameworkCore.Sqlite这行命令来安装NuGet包。 入门级使用方式先创建两个实体类: public class ModelA{ public int Id { get; set; } public string Name { get; set; } public List<ModelB> ModelBs { get; } = new List<ModelB>(); }public class ModelB{ public int Id { get; set; } public string Name { get; set; } public int ModelAId { get; set; } public ModelA modelA { get; set; } }然后创建一个继承自 Microsoft.EntityFrameworkCore.DbContext的上下文类: public class DefaultContext: DbContext{ public DbSet<ModelA> ModelAs { get; set; } public DbSet<ModelB> ModelBs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite("Data Source=blogging.db"); }注意在 OnConfiguration方法里设置连接字符串。 如果是使用的已有数据的数据库,则不需要进行下面的步骤,否则建议执行以下步骤,以便可以由EF Core提供的工具生成数据库: 在 NuGet的控制台界面,输入以下命令: Install-Package Microsoft.EntityFrameworkCore.ToolsAdd-Migration InitialCreateUpdate-Database或者在命令行界面输入: dotnet tool install --global dotnet-efdotnet add package Microsoft.EntityFrameworkCore.Designdotnet ef migrations add InitialCreatedotnet ef database update执行成功之后会在项目根目录下多出以下内容: 这是EF Core保留的迁移记录,以便下次使用。 如果项目根目录里没有 blogging.db 这个SQLite文件的话,会自动创建该文件,同时设置好表;如果有,但不是SQLite的文件,则会报错。 使用工具连接到blogging.db数据库,可以看到 EF自动生成的两个实体类对应表的DDL: CREATE TABLE "ModelBs" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_ModelBs" PRIMARY KEY AUTOINCREMENT, "Name" TEXT NULL, "ModelAId" INTEGER NOT NULL, CONSTRAINT "FK_ModelBs_ModelAs_ModelAId" FOREIGN KEY ("ModelAId") REFERENCES "ModelAs" ("Id") ON DELETE CASCADE );-- auto-generated definitioncreate table ModelAs( Id INTEGER not null constraint PK_ModelAs primary key autoincrement, Name TEXT );先略过自动映射的关系,我们来看看如何使用: var context = new DefaultContext();//添加context.Add(new ModelA { Id = 10, Name = "测试" });context.SaveChanges();//保存数据到数据库中//查询var modelA = context.ModelAs.Where(p => p.Id > 1).First();//更新modelA.Name += DateTime.Now;context.SaveChanges();//删除context.Remove(modelA);context.SaveChanges();context.Dispose();这里简单的演示了一下如何使用,到目前为止EF Core可以满足了入门的开发。当然,EF并不只有这些。下一篇将介绍如何自定义映射关系。 原文地址https://www.cnblogs.com/c7jie/p/12889091.html
MySQL 入门(4):锁 摘要在这篇文章中,我将从上一篇的一个小例子开始,跟你介绍一下InnoDB中的行锁。 在这里,会涉及到一个概念:两阶段加锁协议。 之后,我会介绍行锁中的S锁和X锁,以及这两种锁的作用。 但是我们会发现仅仅有行锁是不能解决幻读问题的,于是我会用例子的方式跟你介绍各种间隙锁。 最后,我会聊一聊粒度更大的表级锁和库锁。 1 行锁在上一篇的文章中,我们用了这个具体的例子来解释MVCC: 假设我们调换一下T5和T6: 此时,T5是没有办法执行的。 原因是这样的:InnoDB在更新一行的时候,需要先获取这一行的行锁。 但是,当一条语句获取了行锁之后,不是这行语句执行完毕就能释放锁,而是要等到这个事务执行完毕,才会释放锁。 这里涉及到了两阶段加锁协议:它规定事务的加锁和解锁分为两个独立的阶段,加锁阶段只能加锁不能解锁,一旦开始解锁,则进入解锁阶段,不能再加锁。 然后我们再来说说共享锁(S锁,读锁)和排他锁(X锁,写锁)。 对于共享锁来说,如果一个事务获取了某一行的共享锁,则这个事务只能读这一行数据,而不能修改,并且其他事务也可以获取这一行数据的共享锁,读取这一行的数据,同样不能修改数据。 对于排它锁,只能被某一个事务获取。并且在获取排它锁之前,这一行数据上不能存在共享锁。一旦某一个事务获取了这一行的排它锁,那么只有这一个事务可以对这一行数据进行读写操作,其他事务对这一行数据的读写操作都会被阻塞。 此外,不仅仅只有更新操作,插入、删除操作也会获取这一行数据的X锁。 在这里我还要再介绍这两个概念:“快照读”和“当前读”。 你可能还会有印象,在上一篇内容中,我提到了所有的更新操作都必须是“当前读”,现在可以解释原理了,在更新一行数据的时候,InnoDB会对需要更新的那行数据加上X锁,直接获取最新的那一行数据。 与之相对的是“快照读”,也就是MVCC中的数据读取方式,利用“快照”来读取数据的方式,可以极大的提高事务的并发度。 但是并不是说select语句就只能读取快照,它也照样可以给需要读取的数据加锁,来读取最新的数据。也就是说,select语句也一样可以“当前读”。 下面这两个select语句,就是分别加了读锁(S锁,共享锁)和写锁(X锁,排他锁)。 mysql> select k from t where id=1 lock in share mode;mysql> select k from t where id=1 for update;注意,由于两阶段加锁协议的存在,如果你采用了一致性读,那么这个锁必须要等事务提交后才能解除。这是牺牲了并发度的一种做法。所以,如果所有的select语句,都加上了S锁,此时的“可重复读”,就变成了“序列化”。 2 间隙锁2.1 幻读问题还记得我们上面提到过的幻读吗? 现在你应该能够理解幻读产生的原因了:因为在插入数据的时候,InnoDB采用的是当前读,而读取数据的时候,由于MVCC的存在,采用的是快照读,这就造成了幻读。 但是我们在上面又提到了,select语句也一样可以采用“当前读”。那么,这样能解决幻读吗? 答案是能解决其中一种情况的幻读。 比如我们在上一篇文章中举的关于幻读的例子: 现在你能理解了,因为这里的select是快照读,而事务B的插入操作对于事务A来说是不可见的。如果在T5时刻,事务A的sql语句是select * from t where v = 0 for update,即采用当前读的话,是可以看得到事务B所提交的数据的,这样的话,就避免了幻读的情况。 那如果在T2时刻,事务A的语句就是select * from t where v = 0 for update会怎么样的? 如果在T2时刻就使用了“当前读”,那么T3时刻事务B是无法进行插入操作的。你可以理解为,T2时刻,InnoDB把v=0的数据,都给加上了一把锁。 因为这行sql语句把v=0的数据行都锁住了,所以没有办法再插入一行v=0的数据。 这听起来似乎没什么不对的,但是你仔细想一想,InnoDB中的行锁,锁住的是已经存在的数据。而对于即将要插入的数据,为什么也会被锁住呢?这是不符合行锁的定义的。 这个时候就可以说到间隙锁了。 简单来讲,就是这条语句不仅会锁住所查询的那行数据,还会把这行数据周围的间隙锁住,不让其他事务插入。 也就是说,行锁是锁住已有的数据,而间隙锁,是锁住即将要插入的位置,不让其他数据插入。 在官方文档有这么一句话: Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the innodb_locks_unsafe_for_binlog system variable (which is now deprecated). 也就是说,间隔锁在“可重复读”事务隔离级别是默认生效的。所以,MySQL在“可重复读”的事务隔离级别下,是有办法解决幻读问题的。 下面我们来看看哪些情况InnoDB会给数据加上间隔锁,并且这里的间隔锁范围有多大,注意,下面列举的四种情况,指的是where条件中的字段的索引类型。 主键索引唯一普通索引非唯一普通索引无索引先定义这么一个表: CREATE TABLE t (id int(11) NOT NULL,a int(11) DEFAULT NULL,b int(11) DEFAULT NULL,c int(11) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY a (a), KEY b (b)) ENGINE=InnoDB;id是主键,a是一个唯一索引,b是一个普通索引,c不包含任何的索引字段。 然后插入以下的这些数据: insert into t values(0,0,0,0),(5,5,5,5),(10,10,10,10);然后我们开始分析各种情况。 2.2 主键索引 因为没有其他的数据,所以主键索引在数据页内的编排如上图,并且含有4个空隙。这里说的“空隙”,指的是数据可以插入的位置。 比如我要插入一个id为3的数据,这条数据就会插入到位于(0,5)这个空隙内。 下面我们开始尝试: 毫无疑问T3时刻的sql语句是会被阻塞的,原因是id = 5的这行数据已经被加锁了。那么,会不会存在有间隙锁呢? 因为这是一个主键索引,InnoDB必须保证id = 5的数据是唯一的,所以对于id=5的周围,比如(0,5)和(5,10),不需要再加间隙锁了。 那么换一个条件再试试,我们查找id大于6且id小于8的数据,此时事务B中的语句同样会被阻塞。 这是因为,在主键索引没有命中的时候,会对所在的空白范围,全部加锁。注意,我这里说的是未命中的所有空白范围,哪怕我这里的查找条件是大于6且小于8,但是加锁的范围不是(6,8),而是(5,10)。 你可以简单的理解为:从查找条件的最小值开始,往前找到第一个索引值;并且从查找条件的最大值开始,往后找到第一个索引值,这个范围就是加锁的范围。 你可能还会有一个疑问,如果是select * from t where id = 8 for update会怎么样呢?这个问题和上面一样,只要未命中,就加范围锁,锁住空隙(5,10)。 总结一下:对于主键索引来说,命中了,就只加行锁;没命中,则对查找范围的最小值往前找第一个主键,查找范围的最大值往后找第一个主键,并对这个范围加上间隙锁。 2.3 唯一索引 对于唯一索引来说,和主键索引其实是差不多的。当索引命中之后,因为唯一索引同样保证了索引的唯一性,所以不需要给这行数据的周围加上间隙锁,只会给命中的数据加锁。 但是这里和主键索引不同的地方是,在给唯一索引a = 5加锁的同时,还会回表,将a = 5对应的主键id = 5这行记录加锁。所以,事务B的修改也同样会被阻塞。 这也是为了防止造成数据不一致的情况,比如我把a = 5的这行数据删了,然后事务B又通过这行数据的主键来对这行数据进行操作。 对于带有范围的查找,和上面主键索引的间隙锁规则是一样的,这里不再赘述。值得注意的是,在唯一索引中,只要命中了,就会相应的给这条索引对应的主键id也加锁。 还需要补充一点,当主键索引和唯一索引直接命中的时候,如下图所示,InnoDB除了给a = 5这行数据加了行锁,还可能给(5, 5)这个间隙加了间隙锁,这样的说法听起来很奇怪。 因为事务A是给a = 5这行数据加了行锁,而行锁只能针对已经存在的数据,不能加到即将插入的数据上;此外,当事务A执行这条语句的时候,事务B是会被阻塞的。直到事务A提交,事务B才会提示唯一索引重复。也就是说,在事务B执行这行语句的时候,是无法访问id = 5这行数据的,事务B不知道id = 5到底存不存在。 所以我才说:当索引直接命中的时候,还会加上这么一个小小的间隙锁。我没有查到这方面的资料,如果你能解释的话,请留言告诉我。 2.4 普通索引对于普通索引来说,与唯一索引最大的区别,就是普通索引不是必须唯一的,也就是说,当插入数据的时候,可能会有重复的情况。 而在上面的内容中我们也发现了一个规律:InnoDB的间隙锁,就是为了防止新插入的数据影响查找结果。 所以对于普通索引来说,还需要防止新插入的数据和原数据一样的情况(因为唯一索引不需要担心这么一种情况)。 下面我们举例说明,在此之前先插入一行数据: insert into t values(8,8,5,8);那么此时我们的索引b,是这样的: 因为是非唯一索引的原因,在两个b = 5的间隙,也能插入数据。 如图所示,我们这次把查找条件换成了b = 5。此时,我们插入的数据id = 1,理论上应该要插入(0,5)这个间隙内,但是由于间隙锁的存在,插入将被阻塞。 换一句话说,只要此时插入的数据b = 5,那么就一定无法插入。 而对于未命中的条件,规则和上文中说到的一样,根据查找条件的最小值往前找到第一个一个索引,再根据这个条件的最大值往后找到第一个索引,构成间隙锁的范围。 此外,与唯一索引一样,所有命中的数据行,都会回表将主键id也锁住。 2.5 无索引 可以看到,我们的查找条件是c = 5,直接命中了数据。此时我们插入的数据是c = 6,看起来和事务A无关,但是出乎意料的是,事务B还是会被阻塞。 直接说结论:对于不含有索引的查找项来说,会锁住所有的间隙和所有的数据。 关于幻读的问题的一些case,到这里就研究完了(但是我不确定有没有遗漏,如果有,还请你留言告诉我)。 在最后还需要说一个概念,行锁与间隔锁,合称next-key lock。并且需要注意的是,只有在可重复读的事务隔离级别中,才会有间隔锁。并且可重复读是遵循两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的时候才释放的。所以,在防止幻读产生的时候,同样降低了并发度。 3 表级锁在上一节说完了行级锁之后,我们再来聊聊表级锁。 表级锁有两种,一种是显式添加的,一种是隐式添加的。 3.1 读写表锁还记得我们在上文中提到的读锁和写锁的特点吗,这点在表锁中是一样的。 给表加上了写锁,意味着只有这个会话拥有读写这个表的权限;给表加上了读锁,才能读取这个表上的数据,并且可以多个线程共享读锁,但是,只有当某个表上没有读锁时,才能给这个表加上写锁。 下面是给表加锁的语法: lock tables table_name read lock tables table_name write3.2 MDLMDL指的是(Metadata Lock),指的是元数据锁。 MDL也分为了读锁和写锁,功能和上面提到的一样。 只不过MDL不需要像表锁那样显式的使用,它会在访问一个表的时候会被自动加上。其中,在某个表对数据进行操作(包括insert,delete,update,select)的时候,会隐式的加上MDL读锁,在修改表的结构的时候,会加上写锁。 这样做的目的是,防止在一个事务操作数据的时候,表结构被另一个事务给修改了。或者在某一个事务修改表结构的时候,不允许其他的事务操作数据。 4 库锁顾名思义,库锁就是对整个数据库实例加锁。 MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。 使用过这个命令之后,相当于对全库增加了一个读锁,此时其他线程的数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句都会被阻塞。 全局锁的典型使用场景是,做全库逻辑备份。当然了,实现这个功能,我们也可以使用“可重复读”的事务隔离级别,做一次快照读,依然可以实现备份的功能。只不过,有些引擎并没有实现这个事务隔离级别。 写在最后首先,谢谢你能看到这里。 在这篇文章中,尤其是间隙锁部分的内容,我没有查到太多的资料,所以很多内容都是我自己的理解。所以如果你发现了一些bad case,请你留言告诉我。又或者你发现了我哪里的理解是不对的,也请你留言告诉我,谢谢! 当然了,如果有哪里是我讲的不够明白的,也欢迎留言交流~ 原文地址https://www.cnblogs.com/hongjijun/p/12880218.html
浅析Spring中AOP的实现原理——动态代理 1|0一、前言最近在复习Spring的相关内容,刚刚大致研究了一下Spring中,AOP的实现原理。这篇博客就来简单地聊一聊Spring的AOP是如何实现的,并通过一个简单的测试用例来验证一下。废话不多说,直接开始。 2|0二、正文 2|12.1 Spring AOP的实现原理 Spring的AOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。 (一)JDK动态代理 Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。然后JDK动态代理需要使用的第二个组件就是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。 (二)CGLib动态代理 JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。CGLib直接操作字节码,生成类的子类,重写类的方法完成代理。 以上就是Spring实现动态的两种方式,下面我们具体来谈一谈这两种生成动态代理的方式。 2|22.2 JDK的动态代理 (一)实现原理 JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类(记住这个结论,后面测试要用)。这就是JDK动态代理大致的实现方式。 (二)优点 JDK动态代理是JDK原生的,不需要任何依赖即可使用;通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;(三)缺点 如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低; 2|32.3 CGLib动态代理 (一)实现原理 CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。 (二)优点 使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类;CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理;(三)缺点 由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢; 2|42.4 通过代码进行测试 (一)测试JDK动态代理 下面我们通过一个简单的例子,来验证上面的说法。首先我们需要一个接口和它的一个实现类,然后再为这个实现类的方法配置切面,看看Spring是否真的使用的是JDK的动态代理。假设接口的名称为Human,而实现类为Student: public interface Human { void display(); } @Component public class Student implements Human { @Override public void display() { System.out.println("I am a student"); } } 然后我们定义一个切面,将这个display方法作为切入点,为它配置一个前置通知,代码如下: @Aspect @Component public class HumanAspect { // 为Student这个类的所有方法,配置这个前置通知 @Before("execution( cn.tewuyiang.pojo.Student.(..))") public void before() { System.out.println("before student"); } } 下面可以开始测试了,我们通过Java类的方式进行配置,然后编写一个单元测试方法: // 配置类 @Configuration @ComponentScan(basePackages = "cn.tewuyiang") @EnableAspectJAutoProxy public class AOPConfig { } // 测试方法 @Test public void testProxy() { ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class); // 注意,这里只能通过Human.class获取,而无法通过Student.class,因为在Spirng容器中, // 因为使用JDK动态代理,Ioc容器中,存储的是一个类型为Human的代理对象 Human human = context.getBean(Human.class); human.display(); // 输出代理类的父类,以此判断是JDK还是CGLib System.out.println(human.getClass().getSuperclass()); } 注意看上面代码中,最长的那一句注释。由于我们需要代理的类实现了接口,则Spring会使用JDK的动态代理,生成的代理类会实现相同的接口,然后创建一个代理对象存储在Spring容器中。这也就是说,在Spring容器中,这个代理bean的类型不是Student类型,而是Human类型,所以我们不能通过Student.class获取,只能通过Human.class(或者通过它的名称获取)。这也证明了我们上面说过的另一个问题,JDK动态代理无法代理没有定义在接口中的方法。假设Student这个类有另外一个方法,它不是Human接口定义的方法,此时就算我们为它配置了切面,也无法将切面织入。而且由于在Spring容器中保存的代理对象并不是Student类型,而是Human类型,这就导致我们连那个不属于Human的方法都无法调用。这也说明了JDK动态代理的局限性。 我们前面说过,JDK动态代理生成的代理类继承了Proxy这个类,而CGLib生成的代理类,则继承了需要进行代理的那个类,于是我们可以通过输出代理对象所属类的父类,来判断Spring使用了何种代理。下面是输出结果: before studentI am a studentclass java.lang.reflect.Proxy // 注意看,父类是Proxy 通过上面的输出结果,我们发现,代理类的父类是Proxy,也就意味着果然使用的是JDK的动态代理。 (二)测试CGLib动态代理 好,测试完JDK动态代理,我们开始测试CGLib动态代理。我们前面说过,只有当需要代理的类没有实现接口时,Spring才会使用CGLib动态代理,于是我们修改Student这个类的定义,不让他实现接口: @Component public class Student { public void display() { System.out.println("I am a student"); } } 由于Student没有实现接口,所以我们的测试方法也需要做一些修改。之前我们是通过Human.class这个类型从Spring容器中获取代理对象,但是现在,由于没有实现接口,所以我们不能再这么写了,而是要写成Student.class,如下: @Test public void testProxy() { ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class); // 修改为Student.class Student student = context.getBean(Student.class); student.display(); // 同样输出父类 System.out.println(student.getClass().getSuperclass()); } 因为CGLib动态代理是生成了Student的一个子类,所以这个代理对象也是Student类型(子类也是父类类型),所以可以通过Student.class获取。下面是输出结果: before studentI am a studentclass cn.tewuyiang.pojo.Student // 此时,父类是Student 可以看到,AOP成功生效,并且代理对象所属类的父类是Student,验证了我们之前的说法。下面我们修改一下Student类的定义,将display方法加上final修饰符,再看看效果: @Component public class Student { // 加上final修饰符 public final void display() { System.out.println("I am a student"); } } // 输出结果如下: I am a student class cn.tewuyiang.pojo.Student 可以看到,输出的父类仍然是Student,也就是说Spring依然使用了CGLib生成代理。但是我们发现,我们为display方法配置的前置通知并没有执行,也就是代理类并没有为display方法进行代理。这也验证了我们之前的说法,CGLib无法代理final方法,因为子类无法重写父类的final方法。下面我们可以试着为Student类加上final修饰符,让他无法被继承,此时看看结果。运行的结果会抛出异常,因为无法生成代理类,这里就不贴出来了,可以自己去试试。 2|52.5 强制Spring使用CGLib 通过上面的测试我们会发现,CGLib的动态代理好像更加强大,而JDK的动态代理却限制颇多。而且前面也提过,CGLib的代理对象,执行代理方法的速度更快,只是生成代理类的效率较低。但是我们使用到的bean大部分都是单例的,并不需要频繁创建代理类,也就是说CGLib应该会更合适。但是为什么Spring默认使用JDK呢?这我也不太清楚,网上也没有找到相关的描述(如果有人知道,麻烦告诉我)。但是据说SpringBoot现在已经默认使用CGLib作为AOP的实现了。 那我们可以强制Spring使用CGLib,而不使用JDK的动态代理吗?答案当然是可以的。我们知道,如果要使用注解(@Aspect)方式配置切面,则需要在xml文件中配置下面一行开启AOP: 如果我们希望只使用CGLib实现AOP,则可以在上面的这一行加点东西: 当然,如果我们是使用Java类进行配置,比如说我们上面用到的AOPConfig这个类,如果是通过这种方式配置,则强制使用CGLib的方式如下: @Configuration @ComponentScan(basePackages = "cn.tewuyiang") // 如下:@EnableAspectJAutoProxy开启AOP, // 而proxyTargetClass = true就是强制使用CGLib @EnableAspectJAutoProxy(proxyTargetClass = true) public class AOPConfig { } 如果我们是在xml文件中配置切面,则可以通过以下方式来强制使用CGLib: 3|0三、总结 上面我们就对Spring中AOP的实现原理做了一个大致的介绍。归根到底,Spring AOP的实现是通过动态代理,并且有两种实现方式,分别是JDK动态代理和CGLib动态代理。Spring默认使用JDK动态代理,只有在类没有实现接口时,才会使用CGLib。 上面的内容若存在错误或者不足,欢迎指正或补充。也希望这篇博客对需要了解Spring AOP的人有所帮助。 4|0四、参考 Spring-4.3.21官方文档——AOPhttps://blog.csdn.net/xlgen157387/article/details/82497594 EOF 本文作者:特务依昂本文链接:https://www.cnblogs.com/tuyang1129/p/12878549.html
Spring Boot Admin简介及实践 问题在若干年前的单体应用时代,我们可以相对轻松地对整个业务项目进行健康检查、指标监控、配置管理等等项目治理。如今随着微服务的发展,我们将大型单体应用按业务模型进行划分,以此形成众多小而自治的微服务,我们品尝到了微服务的甜头:异常隔离、独立部署和发布、服务伸缩、便于协作开发...我们的项目服务更加解耦合,高可用。但与此同时这也给我们带来了很多挑战,众多服务的健康检查、指标监控问题、配置管理、日志聚合问题、异常排查问题等等。我们急切需要一些工具或者手段来尽可能地解决这些问题,从而让我们收获微服务的最大化利益。 来源背景codecentric的Spring Boot Admin是一个社区项目,用于管理和监视您的Spring Boot®应用程序。这些应用程序在我们的Spring Boot Admin Client中注册(通过HTTP),或者是通过Spring Cloud®(例如Eureka,Consul)发现的。 UI只是Spring Boot Actuator端点之上的Vue.js应用程序。 功能介绍Spring Boot Admin提供了很多服务治理方面的功能,利用它能节省我们很多在治理服务方面的时间和精力Spring Boot Admin提供了如下功能(包括但不限于): 显示健康状态及详细信息,如JVM和内存指标、数据源指标、缓存指标跟踪并下载日志文件查看jvm系统-和环境属性查看Spring启动配置属性方便loglevel管理查看线程转储视图http-traces查看http端点查看计划任务查看和删除活动会话(使用spring-session)状态更改通知(通过电子邮件、Slack、Hipchat…)状态变化的事件日志(非持久性)……(and more !)搭建Spring Boot Admin Server在编写本文的时候,Spring Boot Admin的最新版本为: 2.2.2。接下来我将会用此版本来进行演示。基础环境:Jdk 11、Maven、IntelliJ IDEA 引入依赖由于Spring Boot Admin Server可以作为servlet或webflux应用程序运行,因此您需要对此进行决定并添加相应的Spring Boot Starter。在此示例中,我们使用Servlet Web Starter。 <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.2.2</version> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> 添加配置通过在配置中添加@EnableAdminServer来引入Spring Boot Admin Server配置: @Configuration@EnableAutoConfiguration@EnableAdminServerpublic class SpringBootAdminApplication { public static void main(String[] args) { SpringApplication.run(SpringBootAdminApplication.class, args); } }此时我们通过浏览器访问:http://localhost:8080 可以看到我们可以访问到Spring Boot Admin Server的UI界面: 注册客户端Spring boot Admin提供了多种注册客户端服务的方式,要在SBA(Spring Boot Admin)服务器上注册应用程序,您可以直接注册SBA客户端或使用Spring Cloud Discovery(例如Eureka,Consul等)。在SBA服务器端,还有一个使用静态配置的简单选项。本文将演示直接注册、使用Zookeeper、使用Kubernetes来注册发现客户端服务。 直接注册方式引入依赖使用直接注册方式,需要在客户端服务中引入依赖,从而做到直接与SBA服务端通信。 <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.2.2</version> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> 根据开头所述Spring Boot Admin是基于Spring Boot Actuator之上的,所以我们需要引入Spring Boot Actuator相关依赖,关于Spring Boot Actuator,可以参考此篇文章。此外我们需要处理Actuator的安全性,所以引入Spring Security相关依赖。 添加配置接下来我们在项目配置文件中添加相关配置 spring.boot.admin.client.url=http://localhost:8080 (1)management.endpoints.web.exposure.include=* (2)1⃣️:要注册到其中的Spring Boot Admin Server的URL。2⃣️:与Spring Boot 2一样,默认情况下,大多数Actuator(端点)都不通过http公开,在这里我们公开了所有端点。对于生产,您应该仔细选择要公开的端点。 安全性配置@Configurationpublic static class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll() .and().csrf().disable(); } }为了简洁起见,我们暂时禁用安全性。查看有关如何处理端点的安全性,我会在后续文章中演示。此时我们同时运行SBA的服务端和客户端服务,再次访问http://localhost:8080,可以看到我们的客户端服务已经注册进去,并且可以看到客户端服务的一些信息。 Zookeeper服务发现方式我们通过一些服务发现组件对客户端服务进行注册的时候,我们就可以忽略掉客户端服务了,即我们不需要在客户端服务中引入Spring Boot Admin相关依赖,因为服务端可以通过服务发现组件来自动发现客户端服务。 引入依赖我们在SBA服务端以及客户端中引入Zookeeper相关依赖 <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> 添加配置在SBA服务端项目中添加Zookeeper相关配置 spring: cloud: zookeeper: connect-string: 你的Zookeeper地址 boot: admin: discovery: instances-metadata: sba-register: true 因为Zookeeper中可能存在很多服务,而我们只想发现我们关注的服务,此时我们可以通过上述配置来实现,即我们只发现元数据为sba-register: true的客户端服务。在SBA客户端中添加Zookeeper相关配置 spring.cloud.zookeeper.connect-string=你的Zookeeper地址spring.cloud.zookeeper.discovery.metadata.sba-register=true对应SBA服务端的配置,我们指定了Zookeeper的地址,并且指定了该客户端服务的注册元数据为sba-register: true此时我们同时运行SBA的服务端和客户端服务,再次访问SBA服务端地址,可以看到服务端已经通过Zookeeper自动发现客户端服务,并且可以看到客户端服务的一些信息。 Kubernetes服务发现方式如果你是通过基于Kubernetes的容器化部署,Spring Boot Admin也提供了支持,基于Kubernetes的服务发现方式和Zookeeper方式实现大同小异 引入依赖我们在Spring Boot Admin服务端项目中引入Kubernetes相关依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-kubernetes-discovery</artifactId> </dependency> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> Spring Cloud Kubernetes提供使用Kubernetes本机服务的Spring Cloud公共接口实现。此库的主要目标是促进在Kubernetes中运行的Spring Cloud和Spring Boot应用程序的集成。 添加配置@SpringBootApplication@EnableAdminServer@EnableDiscoveryClient@EnableSchedulingpublic class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); } }@EnableDiscoveryClient注解表示启用基于Kubernetes的服务发现,@EnableScheduling注解是必须的,表示定期调用Kubernetes API来刷新正在运行的服务列表,并且仅在启动时执行一次。由于我们一直希望拥有最新的Pod列表(例如,在扩展应用程序实例数量之后),因此我们需要启用调度程序来负责监视服务目录的更改并相应地更新DiscoveryClient实例列表。 Kubenetes权限配置Spring Boot Admin使用Spring Cloud Kubernetes,它需要额外的特权才能访问Kubernetes API。我们仅出于开发目的,将cluster-admin设置为ServiceAccount的默认角色。 $ kubectl create clusterrolebinding admin-default --clusterrole=cluster-admin --serviceaccount=default:default此时我们同时运行SBA的服务端和客户端服务,再次访问SBA服务端,可以看到服务端已经通过Kubernetes自动发现客户端服务,并且可以看到客户端服务的一些信息。 总结本文主要介绍了Spring Boot Admin(SBA)的诞生背景已经其带来的一些功能特性,在这个微服务遍地开花的时代SBA缓解了我们在微服务中遇到的许多棘手的问题。后面本文还用代码演示了如何在项目中引入并使用SBA。本文只涉及到了SBA的基础实践,我会在后续文章中详细演示更多SBA的高级功能,看看我们能从中受益多少。 本文的示例代码SBA-client:https://github.com/cg837718548/sba-client-demo.gitSBA-server:https://github.com/cg837718548/sba-server-demo.git 原文地址https://www.cnblogs.com/dongxishaonian/p/12869770.html
第十一章:Python高级编程-协程和异步IO Python3高级核心技术97讲 笔记 目录第十一章:Python高级编程-协程和异步IO11.1 并发、并行、同步、异步、阻塞、非阻塞11.2 C10K问题和IO多路复用(select、poll、epoll)11.2.1 C10K问题11.2.2 Unix下五种I/O模型11.3 select+回调+事件循环11.4 回调之痛11.5 什么是协程11.5.1 C10M问题11.5.2 协程11.6 生成器进阶-send、close和throw方法11.7生成器进阶-yield from11.8 yield from how11.9 async和await11.10 生成器实现协程11.1 并发、并行、同步、异步、阻塞、非阻塞并发 并发是指一个时间段内,有几个程序在同一个CPU上运行,但是任意时刻只有一个程序在CPU上运行。 并行 并行是指任意时刻点上,有多个程序同时运行在多个CPU上。 同步 同步是指代码调用IO操作是,必须等待IO操作完成才返回的调用方式。 异步 异步是指代码调用IO操作是,不必等IO操作完成就返回的调用方式。 阻塞 阻塞是指调用函数时候当前线程被挂起。 非阻塞 阻塞是指调用函数时候当前线程不会被挂起,而是立即返回。 11.2 C10K问题和IO多路复用(select、poll、epoll)11.2.1 C10K问题如何在一颗1GHz CPU,2G内存,1gbps网络环境下,让单台服务器同时为一万个客户端提供FTP服务。 11.2.2 Unix下五种I/O模型阻塞式IO 非阻塞IO IO复用 信息驱动式IO 异步IO(POSIX的aio_系列函数 select、poll、epoll select、poll、epoll都是IO多路复用的机制。IO多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但是select、poll、epoll本质上都是同步IO,因为他们都需要在读写时间就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步IO则无需自己负责进行读写,异步IO的实现会负责把数据从内核拷贝到用户空间。 select select函数监视的文件描述符分为3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 select目前几乎在所有的平台上支持,其良好跨平台支持也是他的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。 poll 不同于select使用三个位图来表示三个fdset的方式,pollshiyongyigepollfd的指针实现。 pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,需要伦轮询pollfd来获取就绪的描述符 从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。 epoll epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。 11.3 select+回调+事件循环Copy 1. epoll并不代表一定比select好 在并发高的情况下,连接活跃度不是很高, epoll比select 并发性不高,同时连接很活跃, select比epoll好 通过非阻塞io实现http请求 import socketfrom urllib.parse import urlparse 使用非阻塞io完成http请求 def get_url(url): #通过socket请求html url = urlparse(url) host = url.netloc path = url.path if path == "": path = "/" #建立socket连接 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.setblocking(False) try: client.connect((host, 80)) #阻塞不会消耗cpu except BlockingIOError as e: pass #不停的询问连接是否建立好, 需要while循环不停的去检查状态 #做计算任务或者再次发起其他的连接请求 while True: try: client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8")) break except OSError as e: pass data = b"" while True: try: d = client.recv(1024) except BlockingIOError as e: continue if d: data += d else: break data = data.decode("utf8") html_data = data.split("\r\n\r\n")[1] print(html_data) client.close() if name == "__main__": get_url("http://www.baidu.com") Copy 1. epoll并不代表一定比select好 在并发高的情况下,连接活跃度不是很高, epoll比select 并发性不高,同时连接很活跃, select比epoll好 通过非阻塞io实现http请求 select + 回调 + 事件循环 并发性高 使用单线程 import socketfrom urllib.parse import urlparsefrom selectors import DefaultSelector, EVENT_READ, EVENT_WRITE selector = DefaultSelector() 使用select完成http请求 urls = []stop = False class Fetcher: def connected(self, key): selector.unregister(key.fd) self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8")) selector.register(self.client.fileno(), EVENT_READ, self.readable) def readable(self, key): d = self.client.recv(1024) if d: self.data += d else: selector.unregister(key.fd) data = self.data.decode("utf8") html_data = data.split("\r\n\r\n")[1] print(html_data) self.client.close() urls.remove(self.spider_url) if not urls: global stop stop = True def get_url(self, url): self.spider_url = url url = urlparse(url) self.host = url.netloc self.path = url.path self.data = b"" if self.path == "": self.path = "/" # 建立socket连接 self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client.setblocking(False) try: self.client.connect((self.host, 80)) # 阻塞不会消耗cpu except BlockingIOError as e: pass #注册 selector.register(self.client.fileno(), EVENT_WRITE, self.connected) def loop(): #事件循环,不停的请求socket的状态并调用对应的回调函数 #1. select本身是不支持register模式 #2. socket状态变化以后的回调是由程序员完成的 while not stop: ready = selector.select() for key, mask in ready: call_back = key.data call_back(key) #回调+事件循环+select(poll\epoll) if name == "__main__": fetcher = Fetcher() import time start_time = time.time() for url in range(20): url = "http://shop.projectsedu.com/goods/{}/".format(url) urls.append(url) fetcher = Fetcher() fetcher.get_url(url) loop() print(time.time()-start_time) def get_url(url): 通过socket请求html url = urlparse(url) host = url.netloc path = url.path if path == "": path = "/" 建立socket连接 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.setblocking(False) try: client.connect((host, 80)) #阻塞不会消耗cpu except BlockingIOError as e: pass 不停的询问连接是否建立好, 需要while循环不停的去检查状态 做计算任务或者再次发起其他的连接请求 while True: try: client.send("GET {} HTTP/1.1rnHost:{}rnConnection:closernrn".format(path, host).encode("utf8")) break except OSError as e: pass data = b"" while True: try: d = client.recv(1024) except BlockingIOError as e: continue if d: data += d else: break data = data.decode("utf8") html_data = data.split("rnrn")[1] print(html_data) client.close() 11.4 回调之痛如果回调函数执行不正常该如何? 如果回调里面还要嵌套回调该怎么办?要嵌套很多层怎么办? 如果嵌套了多层,其中某个环节出错了会造成什么后果? 如果有个数据需要被每个回调都处理怎么办? .... 可读性差共享状态管理困难异常处理困难11.5 什么是协程11.5.1 C10M问题如何利用8核心CPU,64G内存,在10gbps的网络上保持1000万并发连接 11.5.2 协程Copy def get_url(url): do someting 1 html = get_html(url) #此处暂停,切换到另一个函数去执行 parse html urls = parse_url(html) def get_url(url): do someting 1 html = get_html(url) #此处暂停,切换到另一个函数去执行 parse html urls = parse_url(html) 传统函数调用 过程 A->B->C 我们需要一个可以暂停的函数,并且可以在适当的时候恢复该函数的继续执行 出现了协程 -> 有多个入口的函数, 可以暂停的函数, 可以暂停的函数(可以向暂停的地方传入值) 11.6 生成器进阶-send、close和throw方法Copydef gen_func(): #1. 可以产出值, 2. 可以接收值(调用方传递进来的值) html = yield "http://projectsedu.com" print(html) return "bobby" 1. throw, close 1. 生成器不只可以产出值,还可以接收值 if name == "__main__": gen = gen_func() #在调用send发送非none值之前,我们必须启动一次生成器, 方式有两种1. gen.send(None), 2. next(gen) url = gen.send(None) #download url html = "bobby" print(gen.send(html)) #send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置 print(gen.send(html)) #1.启动生成器方式有两种, next(), send # print(next(gen)) # print(next(gen)) # print(next(gen)) # print(next(gen)) Copydef gen_func(): #1. 可以产出值, 2. 可以接收值(调用方传递进来的值) try: yield "http://projectsedu.com" except BaseException: pass yield 2 yield 3 return "bobby" if name == "__main__": gen = gen_func() print(next(gen)) gen.close() print("bobby") #GeneratorExit是继承自BaseException, Exception Copydef gen_func(): #1. 可以产出值, 2. 可以接收值(调用方传递进来的值) try: yield "http://projectsedu.com" except Exception as e: pass yield 2 yield 3 return "bobby" if name == "__main__": gen = gen_func() print(next(gen)) gen.throw(Exception, "download error") print(next(gen)) gen.throw(Exception, "download error") 11.7生成器进阶-yield fromCopy python3.3新加了yield from语法 from itertools import chain my_list = [1,2,3]my_dict = { "bobby1":"http://projectsedu.com", "bobby2":"http://www.imooc.com", } yield from iterable def g1(iterable): yield iterable def g2(iterable): yield from iterable for value in g1(range(10)): print(value) for value in g2(range(10)): print(value) def my_chain(args, *kwargs): for my_iterable in args: yield from my_iterable # for value in my_iterable: # yield value for value in my_chain(my_list, my_dict, range(5,10)): print(value) def g1(gen): yield from gen def main(): g = g1() g.send(None) 1. main 调用方 g1(委托生成器) gen 子生成器 1. yield from会在调用方与子生成器之间建立一个双向通道 Copyfinal_result = {} def middle(key): while True: final_result[key] = yield from sales_sum(key) print(key+"销量统计完成!!.") def main(): data_sets = { "bobby牌面膜": [1200, 1500, 3000], "bobby牌手机": [28,55,98,108 ], "bobby牌大衣": [280,560,778,70], } for key, data_set in data_sets.items(): print("start key:", key) m = middle(key) m.send(None) # 预激middle协程 for value in data_set: m.send(value) # 给协程传递每一组的值 # 发送到字生成器里 m.send(None) print("final_result:", final_result) if name == '__main__': main() def sales_sum(pro_name): total = 0 nums = [] while True: x = yield print(pro_name+"销量: ", x) if not x: break total += x nums.append(x) return total, nums if name == "__main__": my_gen = sales_sum("bobby牌手机") my_gen.send(None) my_gen.send(1200) my_gen.send(1500) my_gen.send(3000) try: my_gen.send(None) except StopIteration as e: result = e.value print(result) 11.8 yield from howCopy pep380 1. RESULT = yield from EXPR可以简化成下面这样 一些说明 """_i:子生成器,同时也是一个迭代器_y:子生成器生产的值_r:yield from 表达式最终的值_s:调用方通过send()发送的值_e:异常对象 """ _i = iter(EXPR) # EXPR是一个可迭代对象,_i其实是子生成器;try: _y = next(_i) # 预激子生成器,把产出的第一个值存在_y中; except StopIteration as _e: _r = _e.value # 如果抛出了`StopIteration`异常,那么就将异常对象的`value`属性保存到_r,这是最简单的情况的返回值; else: while 1: # 尝试执行这个循环,委托生成器会阻塞; _s = yield _y # 生产子生成器的值,等待调用方`send()`值,发送过来的值将保存在_s中; try: _y = _i.send(_s) # 转发_s,并且尝试向下执行; except StopIteration as _e: _r = _e.value # 如果子生成器抛出异常,那么就获取异常对象的`value`属性存到_r,退出循环,恢复委托生成器的运行; break RESULT = _r # _r就是整个yield from表达式返回的值。 """ 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法; 如果子生成器支持.throw()和.close()方法,但是在子生成器内部,这两个方法都会抛出异常; 调用方让子生成器自己抛出异常 当调用方使用next()或者.send(None)时,都要在子生成器上调用next()函数,当调用方使用.send()发送非 None 值时,才调用子生成器的.send()方法;""" _i = iter(EXPR)try: _y = next(_i) except StopIteration as _e: _r = _e.value else: while 1: try: _s = yield _y except GeneratorExit as _e: try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r """看完代码,我们总结一下关键点: 子生成器生产的值,都是直接传给调用方的;调用方通过.send()发送的值都是直接传递给子生成器的;如果发送的是 None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的.send()方法; 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常; yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数; 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 "冒泡"; 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上 "冒泡"; 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有的话就不调用。如果在调用.close()的时候抛出了异常,那么就向上 "冒泡",否则的话委托生成器会抛出GeneratorExit异常。 """ 11.9 async和awaitCopy python为了将语义变得更加明确,就引入了async和await关键词用于定义原生的协程 async def downloader(url): return "bobby" import types @types.coroutinedef downloader(url): yield "bobby" async def download_url(url): #dosomethings html = await downloader(url) return html if name == "__main__": coro = download_url("http://www.imooc.com") # next(None) coro.send(None) 11.10 生成器实现协程Copy 生成器是可以暂停的函数 import inspect def gen_func(): value=yield from 第一返回值给调用方, 第二调用方通过send方式返回值给gen return "bobby" 1. 用同步的方式编写异步的代码, 在适当的时候暂停函数并在适当的时候启动函数 import socketdef get_socket_data(): yield "bobby" def downloader(url): client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.setblocking(False) try: client.connect((host, 80)) # 阻塞不会消耗cpu except BlockingIOError as e: pass selector.register(self.client.fileno(), EVENT_WRITE, self.connected) source = yield from get_socket_data() data = source.decode("utf8") html_data = data.split("\r\n\r\n")[1] print(html_data) def download_html(html): html = yield from downloader() if name == "__main__": #协程的调度依然是 事件循环+协程模式 ,协程是单线程模式 pass 作者: coderchen01 出处:https://www.cnblogs.com/xunjishu/p/12864396.html
SpringMVC源码学习:容器初始化+MVC初始化+请求分发处理+参数解析+返回值解析+视图解析 目录一、前言二、初始化 容器初始化根容器查找的方法 容器创建的方法加载配置文件信息 MVC的初始化文件上传解析器 区域信息解析器handler映射信息解析 HandlerMapping的实现原理HandlerExecutionChain RequestMappingHandlerMapping三、请求响应处理 请求分发 请求处理参数解析过程 传递页面参数返回值解析 视图解析视图解析器 视图一、前言版本: springMVC 5.0.2RELEASE JDK1.8 前端控制器的配置: web.xml 复制 <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <!--加载类路径下的配置文件--> <param-value>classpath:springmvc.xml</param-value> </init-param> <!--服务器启动时创建对象,值越小,优先级越高,越先创建对象--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <!--注意不是/*,而是,因为/*还会拦截*.jsp等请求--> <url-pattern>/</url-pattern> </servlet-mapping> springmvc.xml配置 复制<?xml version="1.0" encoding="UTF-8"?> xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 开启注解扫描 --> <context:component-scan base-package="com.smday"/> <!-- 视图解析器对象 --> <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 开启SpringMVC框架注解的支持 --> <mvc:annotation-driven/> <!--放行静态资源--> <mvc:default-servlet-handler/> 二、初始化DispatcherServlet的启动与Servlet的启动过程紧密联系,我们通过以上继承图就可以发现。 容器初始化Servlet中定义的init()方法就是其生命周期的初始化方法,接着往下走,GenericServlet并没有给出具体实现,在HttpServletBean中的init()方法给出了具体的实现: HttpServletBean.init()方法(忽略日志) 复制 @Override public final void init() throws ServletException { //根据初始化参数设置bean属性(我们设置了contextConfigLocation,故可以获取) PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { try { //包装DispatcherServlet BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); //获取资源加载器,用以加载springMVC的配置文件 ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); //注册一个ResourceEditor bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); //该方法为空实现,可以重写,初始化BeanWrapper initBeanWrapper(bw); //最终将init-param读取的值spirng-mvc.xml存入contextConfigLocation中 bw.setPropertyValues(pvs, true); } } // 让子类实现初始化 initServletBean(); } 那就来看看FrameworfServlet.initServletBean()干了啥(基本都是日志记录,还有计时,省略了这些部分): 复制 /** * Overridden method of {@link HttpServletBean}, invoked after any bean properties * have been set. Creates this servlet's WebApplicationContext. */ @Override protected final void initServletBean() throws ServletException { //WebApplicationContext的初始化 this.webApplicationContext = initWebApplicationContext(); //也是空实现,允许子类自定义 initFrameworkServlet(); } 所以重头戏就在initWebApplicationContext方法上,我们可以先来看看执行后的效果: 可以看到springMVC九大组件被赋值,除此之外webApplicationContext也已被赋值。 我们再来看看源码,看看其内部具体实现:FrameworkServlet.initWebApplicationContext() 复制protected WebApplicationContext initWebApplicationContext() { //根容器查找 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { //在构建时注入了DispatcherServlet并且webApplicationContext已经存在->直接使用 wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { //如果context还没有refresh-->进行设置父级context以及application context的id等等操作 if (cwac.getParent() == null) { //在没有显式父级的情况下注入了context实例->将根应用程序上下文设置为父级 cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { //在构造时未注入任何上下文实例-->从ServletContext中查询 wac = findWebApplicationContext(); } if (wac == null) { // ServletContext中没有-->就创建一个被本地的 wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { //如果context不支持refresh或者在初始化的时候已经refresh-->就手动触发onfresh onRefresh(wac); } //把当前建立的上下文存入ServletContext中,使用的属性名和当前Servlet名相关 if (this.publishContext) { // 将上下文发布为servlet上下文属性 String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; }根容器查找的方法复制WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContextUtils.getWebApplicationContext 复制//SpringMVC支持Spring容器与Web容易同时存在,并且Spring容器视作根容器,通常由ContextLoaderListener进行加载。@Nullablepublic static WebApplicationContext getWebApplicationContext(ServletContext sc) { //String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT" return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); } @Nullablepublic static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) { //根据ServletName.ROOT为键查找值 Object attr = sc.getAttribute(attrName); if (attr == null) { return null; return (WebApplicationContext) attr; }Spring容器和Web容器如果同时存在,需要使用ContextLoaderListener加载Spring的配置,且它会以key为 WebApplicationContext.class.getName() + ".ROOT存到ServletContext中。 容器创建的方法构建的时候没有任何Context实例注入,且ServletContext中也没有找到WebApplicationContext,此时就会创建一个local Context,这个方法允许显式传入父级容器作为参数。 复制protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { //默认:DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;可以在初始化参数中指定contextClass Class<?> contextClass = getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( "Fatal initialization error in servlet with name '" + getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } //获取ConfigurableWebApplicationContext对象 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } configureAndRefreshWebApplicationContext(wac); return wac; }我们可以发现:在这个过程中,Web容器的IoC容器被建立,也就是XmlWebApplicationContext,,从而在web容器中建立起整个spring应用。 configureAndRefreshWebApplicationContext(wac); 复制protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { //省略给ConfigurableWebApplicationContext对象设置一些值... //每次context refresh,都会调用initPropertySources ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig()); } postProcessWebApplicationContext(wac); applyInitializers(wac); //初始化webApplication容器,重启 wac.refresh(); }加载配置文件信息其实也就是refresh()这个关键方法,之前了解过spring容器的初始化的过程,对这一步应该相当熟悉,还是分为三步: BeanDefinition的Resource的定位,我们这定位到了classpath:springmvc.xml。 beanDefinition的载入过程,springMVC做了一些改变,比如定义了针对mvc的命名空间解析MvcNamespaceHandler。 接着是beanDefinition在IoC中的注册,也就是把beanName:beanDefinition以键值对的形式存入beandefinitionMap中。 MVC的初始化MVC的初始化在DispatcherServlet的initStratefies方法中执行,通过方法名,我们就可以得出结论,就是在这进行了对九大组件的初始化,其实基本上都是从IoC容器中获取对象: 复制 protected void initStrategies(ApplicationContext context) { //文件上传解析器 initMultipartResolver(context); //区域信息解析器,与国际化相关 initLocaleResolver(context); //主题解析器 initThemeResolver(context); //handler映射信息解析 initHandlerMappings(context); //handler的适配器 initHandlerAdapters(context); //handler异常解析器 initHandlerExceptionResolvers(context); //视图名转换器 initRequestToViewNameTranslator(context); //视图解析器 initViewResolvers(context); //flashMap管理器 initFlashMapManager(context); } 文件上传解析器复制private void initMultipartResolver(ApplicationContext context) { try { this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); } catch (NoSuchBeanDefinitionException ex) { // 默认是没有配置multipartResolver的. this.multipartResolver = null; } }配置文件上传解析器也很简单,只需要在容器中注册MultipartResolver即可开启文件上传功能。 区域信息解析器复制private void initLocaleResolver(ApplicationContext context) { try { this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); } catch (NoSuchBeanDefinitionException ex) { // 使用默认策略,利用反射创建对象 this.localeResolver = getDefaultStrategy(context, LocaleResolver.class); } }org.springframework.web.servlet.DispatcherServlet同级目录下的DispatcherServlet.properties文件中规定了几大组件初始化的默认策略。 handler映射信息解析handlerMappings存在的意义在于为HTTP请求找到对应的控制器Controller。 复制 private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; //从所有的IoC容器中导入HandlerMappings,包括其双亲上下文 if (this.detectAllHandlerMappings) { Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); // We keep HandlerMappings in sorted order. AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { //尝试从容器中获取 HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } //保证至少有一个handlerMapping if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); } } 接下来几个操作都差不多,就不赘述了。 总的来说,MVC初始化的过程建立在IoC容器初始化之后,毕竟要从容器中取出这些组件对象。 HandlerMapping的实现原理HandlerExecutionChain HandlerMapping在SpringMVC扮演着相当重要的角色,我们说,它可以为HTTP请求找到 对应的Controller控制器,于是,我们来好好研究一下,这里面到底藏着什么玩意。 HandlerMapping是一个接口,其中包含一个getHandler方法,能够通过该方法获得与HTTP请求对应的handlerExecutionChain,而这个handlerExecutionChain对象中持有handler和interceptorList,以及和设置拦截器相关的方法。可以判断是同通过这些配置的拦截器对handler对象提供的功能进行了一波增强。 RequestMappingHandlerMapping我们以其中一个HandlerMapping作为例子解析一下,我们关注一下: 复制protected void initHandlerMethods() { //获取所有上下文中的beanName String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class)); for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; //得到对应beanName的Class beanType = obtainApplicationContext().getType(beanName); //判断是否为控制器类 if (beanType != null && isHandler(beanType)) { //对控制器中的方法进行处理 detectHandlerMethods(beanName); } } } handlerMethodsInitialized(getHandlerMethods()); }isHandler方法:判断该类是否存在@Controller注解或者@RequestMapping注解 复制 @Override protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); } detectHandlerMethods方法: 复制protected void detectHandlerMethods(final Object handler) { //获取到控制器的类型 Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { //对类型再次进行处理,主要是针对cglib final Class<?> userType = ClassUtils.getUserClass(handlerType); //遍历方法,对注解中的信息进行处理,得到RequestMappingInfo对象,得到methods数组 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> { return getMappingForMethod(method, userType); }); //遍历methods[Method,{path}] for (Map.Entry<Method, T> entry : methods.entrySet()) { //对方法的可访问性进行校验,如private,static,SpringProxy Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType); //获取最终请求路径 T mapping = entry.getValue(); //注册 registerHandlerMethod(handler, invocableMethod, mapping); } } }mapping对象的属性: methods对象中存储的元素: 注册方法在AbstractHandlerMethodMapping中实现: 复制public void register(T mapping, Object handler, Method method) { this.readWriteLock.writeLock().lock(); try { //处理方法的对象 HandlerMethod handlerMethod = createHandlerMethod(handler, method); //判断映射的唯一性 assertUniqueMethodMapping(handlerMethod, mapping); //将mapping信息和控制器方法对应 this.mappingLookup.put(mapping, handlerMethod); //将path与处理器映射(一个方法可能可以处理多个url) List<String> directUrls = getDirectUrls(mapping); for (String url : directUrls) { this.urlLookup.add(url, mapping); } //控制器名的大写英文缩写#方法名 String name = null; if (getNamingStrategy() != null) { name = getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); } //跨域请求相关配置 CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { this.corsLookup.put(handlerMethod, corsConfig); } //将所有配置统一注册到registry中 this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); } finally { this.readWriteLock.writeLock().unlock(); } }至此,所有的Controller,以及其中标注了@RequestMapping注解的方法,都被一一解析,注册进HashMap中,于是,对应请求路径与处理方法就一一匹配,此时HandlerMapping也初始化完成。 三、请求响应处理 请求分发我们需要明确的一个点是,请求过来的时候,最先执行的地方在哪,是Servlet的service方法,我们只需要看看该方法在子类中的一个实现即可: FrameworkServlet重写的service方法: 复制 @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取请求方法 HttpMethod httpMethod = HttpMethod.resolve(request.getMethod()); //拦截PATCH请求 if (HttpMethod.PATCH == httpMethod || httpMethod == null) { processRequest(request, response); } else { super.service(request, response); } } 其实最后都是调用了processRequest方法,该方法中又调用了真正的doService()方法,其中细节先不探讨,我们直奔,看看DispatcherServlet的这个doService干了哪些事情(DispatcherServlet这个类确实是核心中的核心,既建立了IoC容器,又负责请求分发): 复制 @Override protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { //忽略一大串前期准备,使其能够处理view 对象 //接着进入真正的分发 doDispatch(request, response); } doService: 复制protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { //如果是文件上传请求,对request进行包装,如果不是就原样返回 processedRequest = checkMultipart(request); //文件上传请求标识符 multipartRequestParsed = (processedRequest != request); //为当前的request请求寻找合适的handler mappedHandler = getHandler(processedRequest); //如果没有handler可以处理该请求,就跳转到错误页面 if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } //为当前的request请求寻找合适的adapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { //判断是否支持getLastModified,如果不支持,返回-1 long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } //执行注册拦截器的preHandle方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 真正处理请求的方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } //如果mv!=null&&mv对象没有View,则为mv对象设置一个默认的ViewName applyDefaultViewName(processedRequest, mv); //执行注册拦截器的applyPostHandle方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } //进行视图解析和渲染 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); }需要注意的是,mappedHandler和HandlerAdapter都是从对应的集合中遍历查找,一旦找到可以执行的目标,就会停止查找,我们也可以人为定义优先级,决定他们之间的次序。 请求处理RequestMappingHandlerAdapter的handleInternal方法,含有真正处理请求的逻辑。 复制@Overrideprotected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { //定义返回值变量 ModelAndView mav; //对请求进行检查 supportedMethods和requireSession checkRequest(request); // 看看synchronizeOnSession是否开启,默认为false if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); //Httpsession可用 if (session != null) { Object mutex = WebUtils.getSessionMutex(session); //加锁,所有请求串行化 synchronized (mutex) { mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // 没有可用的Httpsession -> 没必要上锁 mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // 正常调用处理方法 mav = invokeHandlerMethod(request, response, handlerMethod); } //检查响应头是否包含Cache-Control if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); } else { prepareResponse(response); } } return mav; }RequestMappingHandlerAdapter的invokeHandlerMethod方法,真正返回mv。 复制@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { //对HttpServletRequest进行包装,产生ServletWebRequest处理web的request对象 ServletWebRequest webRequest = new ServletWebRequest(request, response); try { //创建WebDataBinder对象的工厂 WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); //创建Model对象的工厂 ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); //将handlerMethod对象进行包装,创建ServletInvocableHandlerMethod对象 //向invocableMethod设置相关属性(最后是由invocableMethod对象调用invokeAndHandle方法 ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); //创建ModelAndViewContainer对象,里面存放有向域中存入数据的map ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncWebRequest.setTimeout(this.asyncRequestTimeout); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); //省略异步处理 //正常调用 invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } //获取ModelAndView对象 return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } }ServletInvocableHandlerMethod的invokeAndHandle方法:反射调用方法,得到返回值。 复制public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception { //获取参数,通过反射得到返回值 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); //设置响应状态 setResponseStatus(webRequest); if (returnValue == null) { if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { mavContainer.setRequestHandled(true); return; } } else if (StringUtils.hasText(getResponseStatusReason())) { mavContainer.setRequestHandled(true); return; } mavContainer.setRequestHandled(false); Assert.state(this.returnValueHandlers != null, "No return value handlers"); try { //处理返回值 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); } throw ex; } }参数解析过程我们可以知道的是,传递参数时,可以传递Map,基本类型,POJO,ModelMap等参数,解析之后的结果又如何呢?我们以一个具体的例子举例比较容易分析: 复制 @RequestMapping("/handle03/{id}") public String handle03(@PathVariable("id") String sid, Map<String,Object> map){ System.out.println(sid); map.put("msg","你好!"); return "success"; } 复制/** 获取当前请求的方法参数值。*/ private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception { //获取参数对象 MethodParameter[] parameters = getMethodParameters(); //创建一个同等大小的数组存储参数值 Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = resolveProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } if (this.argumentResolvers.supportsParameter(parameter)) { //参数处理器处理参数(针对不同类型的参数有不同类型的处理参数的策略) args[i] = this.argumentResolvers.resolveArgument( parameter, mavContainer, request, this.dataBinderFactory); continue; } if (args[i] == null) { throw new IllegalStateException(); } return args; }resolveArgument方法: 复制@Override@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { //获取注解的信息 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); //包装parameter对象 MethodParameter nestedParameter = parameter.nestedIfOptional(); //获取@PathVariable指定的属性名 Object resolvedName = resolveStringValue(namedValueInfo.name); // if (resolvedName == null) { throw new IllegalArgumentException( "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); } //根据name从url中寻找并获取参数值 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); //没有匹配 if (arg == null) { //如果有default值,则根据该值查找 if (namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } //如果required为false,则可以不指定name,但默认为true。 else if (namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } //虽然匹配,路径中传入的参数如果是“ ”,且有默认的name,则按照默认处理 else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveStringValue(namedValueInfo.defaultValue); } if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; }getNameValueInfo方法: 复制 private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { //从缓存中获取 NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); if (namedValueInfo == null) { //创建一个namedValueInfo对象 namedValueInfo = createNamedValueInfo(parameter); //如果没有在注解中指定属性名,默认为参数名 namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); //更新缓存 this.namedValueInfoCache.put(parameter, namedValueInfo); } return namedValueInfo; } createNamedValueInfo:获取@PathVariable注解的信息,封装成NamedValueInfo对象 复制 @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); Assert.state(ann != null, "No PathVariable annotation"); return new PathVariableNamedValueInfo(ann); } updateNamedValueInfo: 复制private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { String name = info.name; if (info.name.isEmpty()) { //如果注解中没有指定name,则为参数名 name = parameter.getParameterName(); if (name == null) { throw new IllegalArgumentException( "Name for argument type [" + parameter.getNestedParameterType().getName() + "] not available, and parameter name information not found in class file either."); } } String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); return new NamedValueInfo(name, info.required, defaultValue); }resolveName方法: 参数解析的过程: 根据方法对象,获取参数对象数组,并创建存储参数的数组。遍历参数对象数组,并根据参数解析器argumentResolver解析。如果没有参数解析器,报错。参数解析时,先尝试获取注解的信息,以@PathVariable为例。根据指定的name从url中获取参数值,如果没有指定,则默认为自己传入的参数名。传递页面参数我们可能会通过Map、Model、ModelMap等向域中存入键值对,这部分包含在请求处理中。 我们要关注的是ModelAndViewContainer这个类,它里面默认包含着BindingAwareModelMap。 在解析参数的时候,就已经通过MapMethodProcessor参数处理器初始化了一个BindingAwareModelMap。 当然其实这里重点还是参数解析,至于数据为什么封装进map,就很简单了,无非是反射执行方法的时候,通过put将数据存入,当然最后的数据也就存在于ModelAndViewContainer中。 返回值解析省略寻找返回值解析器的过程,因为返回值为视图名,所以解析器为:ViewNameMethodReturnValueHandler。 复制@Overridepublic void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { //获取视图名 String viewName = returnValue.toString(); //向mavContainer中设置 mavContainer.setViewName(viewName); //是否是isRedirectViewName if (isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null){ // should not happen throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } }isRedirectViewName方法 复制 protected boolean isRedirectViewName(String viewName) { //是否符合自定义的redirectPatterns,或者满足redirect:开头的名字 return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:")); } 最后通过getModelAndView获取mv对象,我们来详细解析一下: 复制@Nullableprivate ModelAndView getModelAndView(ModelAndViewContainer mavContainer,ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { //Promote model attributes listed as @SessionAttributes to the session modelFactory.updateModel(webRequest, mavContainer); //如果请求已经处理完成 if (mavContainer.isRequestHandled()) { return null; } //从mavContainer中获取我们存入的数据map ModelMap model = mavContainer.getModel(); //通过视图名、modelmap、和status创建一个ModelAndView对象 ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request != null) { RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } } return mav; }最后返回的都是ModelAndView对象,包含了逻辑名和模型对象的视图。 返回值解析的过程相对比较简单: 根据返回的参数,获取对应的返回值解析器。获取视图名,如果是需要redirect,则mavContainer.setRedirectModelScenario(true);其他情况下,直接给mvcContainer中的ViewName视图名属性设置上即可。最后将mvcContainer的model、status、viewName取出,创建mv对象返回。【总结】 参数解析、返回值解析两个过程都包含大量的解决策略,其中寻找合适的解析器的过程都是先遍历初始化的解析器表,然后判断是否需要异步处理,判断是否可以处理返回值类型,如果可以的话,就使用该解析器进行解析,如果不行,就一直向下遍历,直到表中没有解析器为止。 视图解析复制private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; // 保证渲染一次,cleared作为标记 if (mv != null && !mv.wasCleared()) { //渲染过程!!! render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } }DispatcherServlet的render方法 复制protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; //获取视图名 String viewName = mv.getViewName(); if (viewName != null) { //通过视图解析器viewResolvers对视图名进行处理,创建view对象 view = resolveViewName(viewName, mv.getModelInternal(), locale, request); } else { view = mv.getView(); } if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); }获取视图解析器,解析视图名: 复制@Nullableprotected View resolveViewName(String viewName, @Nullable Map model,Locale locale, HttpServletRequest request) throws Exception { //这里我们注册的是InternalResourceViewResolver if (this.viewResolvers != null) { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; }UrlBasedViewResolver的createView方法: 复制 @Override protected View createView(String viewName, Locale locale) throws Exception { //如果解析器不能处理所给的view,就返回null,让下一个解析器看看能否执行 if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. if (viewName.startsWith(REDIRECT_URL_PREFIX)) { //判断是否需要重定向 } // Check for special "forward:" prefix. if (viewName.startsWith(FORWARD_URL_PREFIX)) { //判断是否需要转发 } //调用父类的loadView方法 return super.createView(viewName, locale); } 最后返回的视图对象: 视图解析器 viewResolver --实例化 --> view(无状态的,不会有线程安全问题) AbstractView的render方法 复制@Overridepublic void render(@Nullable Map model, HttpServletRequest request,HttpServletResponse response) throws Exception { //获取合并后的map,有我们存入域中的map,还有PathVariable对应的键值等 Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); //根据给定的model渲染内部资源,如将model设置为request的属性 renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); }InternalResourceView的renderMergedOutputModel 复制@Overrideprotected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { //将model中的值设置到request域中 exposeModelAsRequestAttributes(model, request); // 如果有的话,给request设置helpers exposeHelpers(request); // 将目标地址设置到request中 String dispatcherPath = prepareForRendering(request, response); // 获取目标资源(通常是JSP)的RequestDispatcher。 RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); // 如果已经包含或响应已经提交,则执行包含,否则转发。 if (useInclude(request, response)) { response.setContentType(getContentType()); rd.include(request, response); } else { // Note: 转发的资源应该确定内容类型本身。 rd.forward(request, response); } }exposeModelAsRequestAttributes 复制protected void exposeModelAsRequestAttributes(Map model,HttpServletRequest request) throws Exception { //遍历model model.forEach((modelName, modelValue) -> { if (modelValue != null) { //向request中设置值 request.setAttribute(modelName, modelValue); } else { //value为null的话,移除该name request.removeAttribute(modelName); } }); }视图解析器视图解析器(实现ViewResolver接口):将逻辑视图解析为具体的视图对象。 每个视图解析器都实现了Ordered接口,并开放order属性,order越小优先级越高。 按照视图解析器的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出异常。 视图视图(实现View接口):渲染模型数据,将模型数据以某种形式展现给用户。 最终采取的视图对象对模型数据进行渲染render,处理器并不关心,处理器关心生产模型的数据,实现解耦。 作者:天乔巴夏丶出处:https://www.cnblogs.com/summerday152/
python数据统计分析 常用函数库scipy包中的stats模块和statsmodels包是python常用的数据分析工具,scipy.stats以前有一个models子模块,后来被移除了。这个模块被重写并成为了现在独立的statsmodels包。 scipy的stats包含一些比较基本的工具,比如:t检验,正态性检验,卡方检验之类,statsmodels提供了更为系统的统计模型,包括线性模型,时序分析,还包含数据集,做图工具等等。 小样本数据的正态性检验(1) 用途 夏皮罗维尔克检验法 (Shapiro-Wilk) 用于检验参数提供的一组小样本数据线是否符合正态分布,统计量越大则表示数据越符合正态分布,但是在非正态分布的小样本数据中也经常会出现较大的W值。需要查表来估计其概率。由于原假设是其符合正态分布,所以当P值小于指定显著水平时表示其不符合正态分布。 正态性检验是数据分析的第一步,数据是否符合正态性决定了后续使用不同的分析和预测方法,当数据不符合正态性分布时,我们可以通过不同的转换方法把非正太态数据转换成正态分布后再使用相应的统计方法进行下一步操作。 (2) 示例 1234567from scipy import statsimport numpy as np np.random.seed(12345678)x = stats.norm.rvs(loc=5, scale=10, size=80) # loc为均值,scale为方差print(stats.shapiro(x)) 运行结果:(0.9654011726379395, 0.029035290703177452) (3) 结果分析 返回结果 p-value=0.029035290703177452,比指定的显著水平(一般为5%)小,则拒绝假设:x不服从正态分布。 检验样本是否服务某一分布(1) 用途 科尔莫戈罗夫检验(Kolmogorov-Smirnov test),检验样本数据是否服从某一分布,仅适用于连续分布的检验。下例中用它检验正态分布。 (2) 示例 1234567from scipy import statsimport numpy as np np.random.seed(12345678)x = stats.norm.rvs(loc=0, scale=1, size=300)print(stats.kstest(x,'norm')) 运行结果:KstestResult(statistic=0.0315638260778347, pvalue=0.9260909172362317) (3) 结果分析 生成300个服从N(0,1)标准正态分布的随机数,在使用k-s检验该数据是否服从正态分布,提出假设:x从正态分布。最终返回的结果,p-value=0.9260909172362317,比指定的显著水平(一般为5%)大,则我们不能拒绝假设:x服从正态分布。这并不是说x服从正态分布一定是正确的,而是说没有充分的证据证明x不服从正态分布。因此我们的假设被接受,认为x服从正态分布。如果p-value小于我们指定的显著性水平,则我们可以肯定的拒绝提出的假设,认为x肯定不服从正态分布,这个拒绝是绝对正确的。 4.方差齐性检验(1) 用途 方差反映了一组数据与其平均值的偏离程度,方差齐性检验用以检验两组或多组数据与其均值偏离程度是否存在差异,也是很多检验和算法的先决条件。 (2) 示例 12345678from scipy import statsimport numpy as np np.random.seed(12345678)rvs1 = stats.norm.rvs(loc=5,scale=10,size=500) rvs2 = stats.norm.rvs(loc=25,scale=9,size=500)print(stats.levene(rvs1, rvs2)) 运行结果:LeveneResult(statistic=1.6939963163060798, pvalue=0.19337536323599344) (3) 结果分析 返回结果 p-value=0.19337536323599344, 比指定的显著水平(假设为5%)大,认为两组数据具有方差齐性。 图形描述相关性(1) 用途 最常用的两变量相关性分析,是用作图描述相关性,图的横轴是一个变量,纵轴是另一变量,画散点图,从图中可以直观地看到相关性的方向和强弱,线性正相关一般形成由左下到右上的图形;负相关则是从左上到右下的图形,还有一些非线性相关也能从图中观察到。 (2) 示例 1234import statsmodels.api as smimport matplotlib.pyplot as pltdata = sm.datasets.ccard.load_pandas().dataplt.scatter(data['INCOMESQ'], data['INCOME']) (3) 结果分析 从图中可以看到明显的正相关趋势。 正态资料的相关分析(1) 用途 皮尔森相关系数(Pearson correlation coefficient)是反应俩变量之间线性相关程度的统计量,用它来分析正态分布的两个连续型变量之间的相关性。常用于分析自变量之间,以及自变量和因变量之间的相关性。 (2) 示例 12345678from scipy import statsimport numpy as np np.random.seed(12345678)a = np.random.normal(0,1,100)b = np.random.normal(2,2,100)print(stats.pearsonr(a, b)) 运行结果:(-0.034173596625908326, 0.73571128614545933) (3) 结果分析 返回结果的第一个值为相关系数表示线性相关程度,其取值范围在[-1,1],绝对值越接近1,说明两个变量的相关性越强,绝对值越接近0说明两个变量的相关性越差。当两个变量完全不相关时相关系数为0。第二个值为p-value,统计学上,一般当p-value<0.05时,可以认为两变量存在相关性。 非正态资料的相关分析(1) 用途 斯皮尔曼等级相关系数(Spearman’s correlation coefficient for ranked data ),它主要用于评价顺序变量间的线性相关关系,在计算过程中,只考虑变量值的顺序(rank, 秩或称等级),而不考虑变量值的大小。常用于计算类型变量的相关性。 (2) 示例 12345from scipy import statsimport numpy as np print(stats.spearmanr([1,2,3,4,5], [5,6,7,8,7])) 运行结果:SpearmanrResult(correlation=0.82078268166812329, pvalue=0.088587005313543812) (3) 结果分析 返回结果的第一个值为相关系数表示线性相关程度,本例中correlation趋近于1表示正相关。第二个值为p-value,p-value越小,表示相关程度越显著。 单样本T检验(1) 用途 单样本T检验,用于检验数据是否来自一致均值的总体,T检验主要是以均值为核心的检验。注意以下几种T检验都是双侧T检验。 (2) 示例 1234567from scipy import statsimport numpy as np np.random.seed(12345678)rvs = stats.norm.rvs(loc=5, scale=10, size=(100,2))print(stats.ttest_1samp(rvs, [1, 5])) 运行结果:Ttest_1sampResult(statistic=array([ 5.12435977, 1.07927393]), pvalue=array([ 1.47820719e-06, 2.83088106e-01])) (3) 结果分析 本例中生成了2列100行的数组,ttest_1samp的第二个参数是分别对两列估计的均值,p-value返回结果,第一列1.47820719e-06比指定的显著水平(一般为5%)小,认为差异显著,拒绝假设;第二列2.83088106e-01大于指定显著水平,不能拒绝假设:服从正态分布。 两独立样本T检验(1) 用途 有于比较两组数据是否来自于同一正态分布的总体。注意:如果要比较的两组数据不满足方差齐性, 需要在ttest_ind()函数中添加参数equal_var = False。 (2) 示例 12345678from scipy import statsimport numpy as np np.random.seed(12345678)rvs1 = stats.norm.rvs(loc=5,scale=10,size=500) rvs2 = stats.norm.rvs(loc=6,scale=10,size=500)print(stats.ttest_ind(rvs1,rvs2)) 运行结果:Ttest_indResult(statistic=-1.3022440006355476, pvalue=0.19313343989106416) (3) 结果分析 返回结果的第一个值为统计量,第二个值为p-value,pvalue=0.19313343989106416,比指定的显著水平(一般为5%)大,不能拒绝假设,两组数据来自于同一总结,两组数据之间无差异。 配对样本T检验(1) 用途 配对样本T检验可视为单样本T检验的扩展,检验的对象由一群来自正态分布独立样本更改为二群配对样本观测值之差。它常用于比较同一受试对象处理的前后差异,或者按照某一条件进行两两配对分别给与不同处理的受试对象之间是否存在差异。 (2) 示例 12345678from scipy import statsimport numpy as np np.random.seed(12345678)rvs1 = stats.norm.rvs(loc=5,scale=10,size=500)rvs2 = (stats.norm.rvs(loc=5,scale=10,size=500) + stats.norm.rvs(scale=0.2,size=500))print(stats.ttest_rel(rvs1,rvs2))运行结果:Ttest_relResult(statistic=0.24101764965300979, pvalue=0.80964043445811551) (3) 结果分析 返回结果的第一个值为统计量,第二个值为p-value,pvalue=0.80964043445811551,比指定的显著水平(一般为5%)大,不能拒绝假设。 单因素方差分析(1) 用途 方差分析(Analysis of Variance,简称ANOVA),又称F检验,用于两个及两个以上样本均数差别的显著性检验。方差分析主要是考虑各组之间的均数差别。 单因素方差分析(One-wayAnova),是检验由单一因素影响的多组样本某因变量的均值是否有显著差异。 当因变量Y是数值型,自变量X是分类值,通常的做法是按X的类别把实例成分几组,分析Y值在X的不同分组中是否存在差异。 (2) 示例 123456from scipy import statsa = [47,56,46,56,48,48,57,56,45,57] # 分组1b = [87,85,99,85,79,81,82,78,85,91] # 分组2c = [29,31,36,27,29,30,29,36,36,33] # 分组3print(stats.f_oneway(a,b,c)) 运行结果:F_onewayResult(statistic=287.74898314933193, pvalue=6.2231520821576832e-19) (3) 结果分析 返回结果的第一个值为统计量,它由组间差异除以组间差异得到,上例中组间差异很大,第二个返回值p-value=6.2231520821576832e-19小于边界值(一般为0.05),拒绝原假设, 即认为以上三组数据存在统计学差异,并不能判断是哪两组之间存在差异 。只有两组数据时,效果同 stats.levene 一样。 多因素方差分析(1) 用途 当有两个或者两个以上自变量对因变量产生影响时,可以用多因素方差分析的方法来进行分析。它不仅要考虑每个因素的主效应,还要考虑因素之间的交互效应。 (2) 示例 123456789101112131415161718192021from statsmodels.formula.api import olsfrom statsmodels.stats.anova import anova_lmimport pandas as pd X1 = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]X2 = [1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2]Y = [76,78,76,76,76,74,74,76,76,55,65,90,65,90,65,90,90,79,70,90, 88,76,76,76,56,76,76,98,88,78,65,67,67,87,78,56,54,56,54,56] data = {'T':X1, 'G':X2, 'L':Y}df = pd.DataFrame(data)formula = 'L~T+G+T:G' # 公式 model = ols(formula,df).fit()print(anova_lm(model))'''运行结果: df sum_sq mean_sq F PR(>F)T 1.0 265.225 265.225000 2.444407 0.126693G 1.0 207.025 207.025000 1.908016 0.175698T:G 1.0 1050.625 1050.625000 9.682932 0.003631Residual 36.0 3906.100 108.502778 NaN NaN''' (3) 结果分析 上述程序定义了公式,公式中,"~"用于隔离因变量和自变量,”+“用于分隔各个自变量, ":"表示两个自变量交互影响。从返回结果的P值可以看出,X1和X2的值组间差异不大,而组合后的T:G的组间有明显差异。 卡方检验(1) 用途 上面介绍的T检验是参数检验,卡方检验是一种非参数检验方法。相对来说,非参数检验对数据分布的要求比较宽松,并且也不要求太大数据量。卡方检验是一种对计数资料的假设检验方法,主要是比较理论频数和实际频数的吻合程度。常用于特征选择,比如,检验男人和女人在是否患有高血压上有无区别,如果有区别,则说明性别与是否患有高血压有关,在后续分析时就需要把性别这个分类变量放入模型训练。 基本数据有R行C列, 故通称RC列联表(contingency table), 简称RC表,它是观测数据按两个或更多属性(定性变量)分类时所列出的频数表。 (2) 示例 1234567891011121314import numpy as npimport pandas as pdfrom scipy.stats import chi2_contingency np.random.seed(12345678)data = np.random.randint(2, size=(40, 3)) # 2个分类,50个实例,3个特征data = pd.DataFrame(data, columns=['A', 'B', 'C'])contingency = pd.crosstab(data['A'], data['B']) # 建立列联表print(chi2_contingency(contingency)) # 卡方检验'''运行结果:(0.36556036556036503, 0.54543425102570975, 1,array([[ 10.45, 8.55], [ 11.55, 9.45]]))''' (3) 结果分析 卡方检验函数的参数是列联表中的频数,返回结果第一个值为统计量值,第二个结果为p-value值,p-value=0.54543425102570975,比指定的显著水平(一般5%)大,不能拒绝原假设,即相关性不显著。第三个结果是自由度,第四个结果的数组是列联表的期望值分布。 单变量统计分析(1) 用途 单变量统计描述是数据分析中最简单的形式,其中被分析的数据只包含一个变量,不处理原因或关系。单变量分析的主要目的是通过对数据的统计描述了解当前数据的基本情况,并找出数据的分布模型。 单变量数据统计描述从集中趋势上看,指标有:均值,中位数,分位数,众数;从离散程度上看,指标有:极差、四分位数、方差、标准差、协方差、变异系数,从分布上看,有偏度,峰度等。需要考虑的还有极大值,极小值(数值型变量)和频数,构成比(分类或等级变量)。 此外,还可以用统计图直观展示数据分布特征,如:柱状图、正方图、箱式图、频率多边形和饼状图。 多元线性回归(1) 用途 多元线性回归模型(multivariable linear regression model ),因变量Y(计量资料)往往受到多个变量X的影响,多元线性回归模型用于计算各个自变量对因变量的影响程度,可以认为是对多维空间中的点做线性拟合。 (2) 示例 12345678910111213141516171819202122232425262728293031import statsmodels.api as smdata = sm.datasets.ccard.load_pandas().datamodel = sm.OLS(endog = data['AVGEXP'], exog = data[['AGE','INCOME','INCOMESQ','OWNRENT']]).fit()print(model.summary())'''运行结果: OLS Regression Results Dep. Variable: AVGEXP R-squared: 0.543Model: OLS Adj. R-squared: 0.516Method: Least Squares F-statistic: 20.22Date: Thu, 31 Jan 2019 Prob (F-statistic): 5.24e-11Time: 15:11:29 Log-Likelihood: -507.24No. Observations: 72 AIC: 1022.Df Residuals: 68 BIC: 1032.Df Model: 4 Covariance Type: nonrobust coef std err t P>|t| [0.025 0.975] AGE -6.8112 4.551 -1.497 0.139 -15.892 2.270INCOME 175.8245 63.743 2.758 0.007 48.628 303.021INCOMESQ -9.7235 6.030 -1.613 0.111 -21.756 2.309 OWNRENT 54.7496 80.044 0.684 0.496 -104.977 214.476 Omnibus: 76.325 Durbin-Watson: 1.692Prob(Omnibus): 0.000 Jarque-Bera (JB): 649.447Skew: 3.194 Prob(JB): 9.42e-142 Kurtosis: 16.255 Cond. No. 87.5 ''' (3) 结果分析 直接通过返回结果中各变量的P值与0.05比较,来判定对应的解释变量的显著性,P<0.05则认为自变量具有统计学意义,从上例中可以看到收入INCOME最有显著性。 逻辑回归(1) 用途 当因变量Y为2分类变量(或多分类变量时)可以用相应的logistic回归分析各个自变量对因变量的影响程度。 (2) 示例 12345678910111213141516171819202122232425262728import statsmodels.api as smdata = sm.datasets.ccard.load_pandas().datadata['OWNRENT'] = data['OWNRENT'].astype(int)model = sm.Logit(endog = data['OWNRENT'], exog = data[['AVGEXP','AGE','INCOME','INCOMESQ']]).fit()print(model.summary())'''运行结果:Optimization terminated successfully. Current function value: 0.504920 Iterations 8 Logit Regression Results Dep. Variable: OWNRENT No. Observations: 72Model: Logit Df Residuals: 68Method: MLE Df Model: 3Date: Fri, 01 Feb 2019 Pseudo R-squ.: 0.2368Time: 17:05:47 Log-Likelihood: -36.354converged: True LL-Null: -47.633 LLR p-value: 4.995e-05 coef std err z P>|z| [0.025 0.975] AVGEXP 0.0002 0.001 0.228 0.820 -0.002 0.002AGE 0.0853 0.042 2.021 0.043 0.003 0.168INCOME -2.5798 0.822 -3.137 0.002 -4.191 -0.968 INCOMESQ 0.4243 0.126 3.381 0.001 0.178 0.670 ''' (3) 结果分析 直接通过返回结果中各变量的P值与0.05比较,来判定对应的解释变量的显著性,P<0.05则认为自变量具有统计学意义。 原文地址https://www.cnblogs.com/guran0823/p/12841441.html
JVM之类加载器、加载过程及双亲委派机制 Java 虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。 1|2虚拟机的执行一个运行中的 Java 虚拟机有着一个清晰的任务:执行 Java 程序。程序开始执行时他才运行,程序结束时他就停止。执行一个所谓的 Java 程序的时候,真真正正在执行的是一个叫做 Java 虚拟机的进程。1|3虚拟机的退出有如下的几种情况: 程序正常执行结束程序在执行过程中遇到了异常或错误而异常终止由于操作系统出现错误而导致 Java 虛拟机进程终止某线程调用 Runtime 类或 System 类的 exit 方法,或 Runtime 类的 halt 方法,并且 Java 安全管理器也允许这次 exit 或 halt 操作。除此之外,JNI ( Java Native Interface) 规范描述了用 JNI Invocation API 来加载或卸载 Java 虛拟机时,Java虛拟机的退出情况。2|0类加载器子系统的作用 类加载器子系统负责从文件系统或者网络中加载 class 文件,class文件的开头有特定的文件标识(CA FE BA BE)ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是 class 文件中常量池部分的内存映射)3|0类加载器 ClassLoader 的作用 .class 文件 → JVM → 最终成为元数据模板,在这一过程中,ClassLoader 充当了运输工具,扮演了一个快递员的角色 4|0类的加载过程 4|1加载(Loading)1、通过一个类的全限定类名获取定义此类的二进制字节流 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区里这个类的各种数据的访问入口 4|2链接(Linking)验证确保 class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全包括四种验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证准备为类变量分配内存并且设置该类变量的默认初始值,即零值。这里不包含用 final 修饰的静态常量 ,因为 final 在编译的时候就会分配了,准备阶段会显式初始化这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java 堆中。解析将常量池内的符号引用转换为直接引用的过程。事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行。符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的 Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT Methodref_info等。4|3初始化(Initialization)初始化阶段就是执行类构造器方法 () 的过程。此方法不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在源文件中出现的顺序执行。() 不同于类的构造器。(关联: 构造器是虚拟机视角下的 () )若该类具有父类,JVM 会保证子类的 () 执行前,父类的 () 已经执行完毕。虚拟机必须保证一个类的 () 方法在多线程下被同步加锁。5|0类加载器的分类JVM 支持两种类型的类加载器:引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。 这里说的自定义类加载器并不是指由开发人员自定义的类加载器,在 Java 虚拟机规范中,将所有派生于抽象类ClassLoader 的所有类加载器都称为自定义类加载器。所以,无论类加载器的类型如何划分,在程序中,我们最常见的类加载器始终只有 3 个,如下图所示: 这四者之间是包含的关系,不是上下层,也不是子父类继承关系 启动类加载器(引导类加载器,Bootstrap ClassLoader)这个类加载器使用 C/C++ 编写,嵌套在 JVM 内部它用来加载 Java 核心类库 ( JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path 路径下的内容),用于提供 Java 自身需要的类并不继承 ClassLoader,没有父加载器加载扩展类和应用程序类加载器,并指定为它们的父加载器出于安全考虑,bootstrap 启动类加载器只加载包名为 java、javax、sun 开头的类扩展类加载器(Extension ClassLoader)Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现派生于 ClassLoader 类父类加载器为启动类加载器从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载。应用程序类加载器(系统类加载器,App ClassLoader)Java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现派生于 ClassLoader 类父类加载器为扩展类加载器它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库该类加载器是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载通过 ClassLoader.getSystemClassLoader() 方法可以获取到该类加载器用户自定义类加载器在 Java 的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 6|0获取 ClassLoader 的途径方式 代码获取当前类的 ClassLoader clazz.getClassLoader()获取当前线程上下文的 ClassLoader Thread.currentThread().getContextClassLoader()获取系统的 ClassLoader ClassLoader.getSystemClassLoader()获取调用者的 ClassLoader DriverManager.getCallerClassLoader()7|0双亲委派机制Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成 class 对象。而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。 7|1工作原理 1、如果一个类加载器收到类加载请求,它不会自己先加载,而是委托给父类的加载器去执行 2、如果父类加载器还存在父类加载器,则进一步委托,直到到达最顶层的类加载器 3、如果父类加载器可以完成类的加载任务,就成功返回,如果不能完成再回退到子类加载器判断 7|2作用1、避免类的重复加载 比如A、B类都需要加载 String 类,如果不用委托而是自己加载自己的,则会在内存中生成两份字节码。 2、保护程序安全,防止核心 API 被随意篡改 比如我们在自定义一个 java.lang.String 类,执行 main 方法的时候会报错,因为 String 是 java.lang 包下的类,应该由启动类加载器加载。 / 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application / public class String { public static void main(String[] args) { System.out.println("hello string"); } } 如果在 java.lang 包中定义 Jdk 中不存在的类呢?依然会报错 / 异常:java.lang.SecurityException: Prohibited package name: java.lang / public class Other { public static void main(String[] args) { System.out.println("hello other"); } } EOF 本文作者:JLSong本文链接:https://www.cnblogs.com/songjilong/p/12834729.html
redis 6.0 redis-cluster-proxy代理尝试 伴随着Redis6.0的发布,作为最令人怦然心动的特性之一,Redis官方同时推出Redis集群的proxy了:redis-cluster-proxy,https://github.com/RedisLabs/redis-cluster-proxy相比从前访问Redis集群时需要制定集群中所有的IP节点相比:1,redis的redis-cluster-proxy实现了redis cluster集群节点的代理(屏蔽),类似于VIP但又比VIP简单,客户端不需要知道集群中的具体节点个数和主从身份,可以直接通过代理访问集群。2,不仅如此,还是具有一些非常实用的改进,比如在redis集群模式下,增加了对multiple操作的支持,跨slot操作等等(有点关系数据库的分库分表中间件的感觉)。redis-cluster-proxy主要特性以下信息来自于官方的说明:redis-cluster-proxy是Redis集群的代理。Redis能够在基于自动故障转移和分片的集群模式下运行。这种特殊模式(指Redis集群模式)需要使用特殊的客户端来理解集群协议:通过代理,集群被抽象了出来,可以实现像单实例一样实现redis集群的访问。Redis集群代理是多线程的,默认情况下,它目前使用多路复用通信模型,这样每个线程都有自己的集群连接,所有属于线程本身的客户端都可以共享该连接。无论如何,在某些特殊情况下(多事务或阻塞命令),多路复用被禁用,客户端将拥有自己的集群连接。通过这种方式,只发送简单命令(比如get和set)的客户端将不需要一组到Redis集群的私有连接。Redis集群代理的主要特点如下:1,自动化路由:每个查询被自动路由到集群的正确节点2,多线程(它目前使用多路复用通信模型,这样每个线程都有自己的集群连接)3,支持多路复用和私有连接模型4,即使在多路复用上下文中,查询执行和应答顺序也是有保证的5,在请求/重定向错误后自动更新集群配置:当这些类型的错误发生在应答中时,代理通过获取集群的更新配置并重新映射所有slot,自动更新集群的内部表示。 所有查询将在更新完成后重新执行,因此,从客户机的角度来看,一切都将正常运行(客户机不会收到ASK|重定向错误:在更新集群配置之后,它们将直接收到预期的结果)。6,跨slot/跨节点查询:支持许多涉及属于不同slot(甚至不同集群节点)的多个键的命令。 这些命令将把查询分成多个查询,这些查询将被路由到不同的槽/节点。 这些命令的应答处理是特定于命令的。有些命令,如MGET,将合并所有应答,就好像它们是单个应答一样。 其他命令(如MSET或DEL)将汇总所有应答的结果。由于这些查询实际上破坏了命令的原子性,所以它们的使用是可选的(默认情况下禁用)。7,一些没有特定节点/slot的命令(如DBSIZE)被传递给所有节点,为了给出所有应答中包含的所有值的和,应答将被映射简化。8,可用于执行某些特定于代理的操作的附加代理命令。 Redis 6.0以及redis-cluster-proxy gcc 5+编译环境依赖 Redis 6.0以及redis-cluster-proxy的编译依赖于gcc 5+,centos 7上的默认gcc版本是4.+,无法满足编译要求,在编译时候会出现类似如下的错误server.h:1022:5: error: expected specifier-qualifier-list before '_Atomic类似错误参考这里:https://wanghenshui.github.io/2019/12/31/redis-ce解决方案参考,笔者环境为centos7,为此折腾了小半天1,https://stackoverflow.com/questions/55345373/how-to-install-gcc-g-8-on-centos,测试可行2,https://blog.csdn.net/displayMessage/article/details/85602701 gcc源码包编译安装,120MB的源码包,有人说是需要40分钟,笔者机器上编译超过了1个小时仍未果,因此采用的是上一种方法 Redis集群环境搭建测试环境拓扑图,如下所示,基于docker的3主3从6个节点的redis cluster集群redis cluster 集群信息,参考之前的文章,redis cluster 自动化安装、扩容和缩容,快速实现Redis集群搭建 redis-cluster-proxy 安装安装步骤: 1,git clone https://github.com/artix75/redis-cluster-proxycd redis-cluster-proxy2,解决gcc版本依赖问题,笔者折腾了好久,gcc 5.0+ 源码包编译安装花了一个多小时未果。后来尝试如下这种方法可行,参考https://stackoverflow.com/questions/55345373/how-to-install-gcc-g-8-on-centos On CentOS 7, you can install GCC 8 from Developer Toolset. First you need to enable the Software Collections repository:yum install centos-release-scl Then you can install GCC 8 and its C++ compiler:yum install devtoolset-8-gcc devtoolset-8-gcc-c++ To switch to a shell which defaults gcc and g++ to this GCC version, use:scl enable devtoolset-8 -- bash You need to wrap all commands under the scl call, so that the process environment changes performed by this command affect all subshells. For example, you could use the scl command to invoke a shell script that performs the required actions. 3,make PREFIX=/usr/local/redis_cluster_proxy install 4,关于rediscluster-proxy配置文件启动的时候可以直接在命令行中指定参数,但最好是使用配置文件模式启动,配置文件中的节点如下,很清爽,注释也很清晰,简单备注了一下,期待发现更多的新特性。 Redis Cluster Proxy configuration file example. 如果指定以配置文件的方式启动,必须指定-c 参数 ./redis-cluster-proxy -c /path/to/proxy.conf INCLUDES Include one or more other config files here. Include files can include other files. 指定配置文件的路径 If instead you are interested in using includes to override configuration options, it is better to use include as the last line. include /path/to/local.conf include /path/to/other.conf CLUSTER ENTRY POINT ADDRESS Indicate the entry point address in the same way it can be indicated in the redis cluster集群自身节点信息,这里是3主3从的6个节点,分别是192.168.0.61~192.168.0.66 redis-cluster-proxy command line arguments. Note that it can be overridden by the command line argument itself. You can also specify multiple entry-points, by adding more lines, ie: cluster 127.0.0.1:7000 cluster 127.0.0.1:7001 You can also use the "entry-point" alias instead of cluster, ie: entry-point 127.0.0.1:7000 cluster 127.0.0.1:7000 cluster 192.168.0.61:8888cluster 192.168.0.62:8888cluster 192.168.0.63:8888cluster 192.168.0.64:8888cluster 192.168.0.65:8888cluster 192.168.0.66:8888 MAIN Set the port used by Redis Cluster Proxy to listen to incoming connections redis-cluster-proxy 端口号指定 from clients (default 7777) port 7777 IP地址绑定,这里指定为redis-proxy-cluster所在节点的IP地址 If you want you can bind a single interface, if the bind option is not specified all the interfaces will listen for incoming connections. You can also bind on multiple interfaces by declaring bind on multiple lines bind 127.0.0.1 bind 192.168.0.12 socket 文件路径 Specify the path for the Unix socket that will be used to listen for incoming connections. There is no default, so Redis Cluster Proxy won't listen on a Unix socket when not specified. unixsocket /path/to/proxy.socket Set the Unix socket file permissions (default 0) unixsocketperm 760 线程数量 Set the number of threads. threads 8 Set the TCP keep-alive value on the Redis Cluster Proxy's socket tcpkeepalive 300 Set the TCP backlog on the Redis Cluster Proxy's socket tcp-backlog 511 连接池信息 Size of the connections pool used to provide ready-to-use sockets to private connections. The number (size) indicates the number of starting connections in the pool. Use 0 to disable connections pool at all. Every thread will have its pool of ready-to-use connections. When the proxy starts, every thread will populate a pool containing connections to all the nodes of the cluster. Whenever a client needs a private connection, it can take a connection from the pool, if available. This will speed-up the client transition from the thread's shared connection to its own private connection, since the connection from the thread's pool should be already connected and ready-to-use. Otherwise, clients with priovate connections must re-connect the the nodes of the cluster (this re-connection will act in a 'lazy' way). connections-pool-size 10 Minimum number of connections in the the pool. Below this value, the thread will start re-spawning connections at the defined rate until the pool will be full again. connections-pool-min-size 10 Interval in milliseconds used to re-spawn connections in the pool. Whenever the number of connections in the pool drops below the minimum (see 'connections-pool-min-size' above), the thread will start re-spawing connections in the pool, until the pool will be full again. New connections will be added at this specified interval. connections-pool-spawn-every 50 Number of connections to re-spawn in the pool at every cycle that will happen with an interval defined by 'connections-pool-spawn-every' (see above). connections-pool-spawn-rate 50 运行模式,一开始最好指定为no,运行时直接打印出来启动日志或者异常信息,这样可以方便地查看启动异常 非常奇怪的是:笔者一开始指定为yes,异常日志输出到文件,竟然跟直接打印日志输出的信息不一致 Run Redis Cluster Proxy as a daemon. daemonize yes pid 文件指定 If a pid file is specified, the proxy writes it where specified at startup and removes it at exit. When the proxy runs non daemonized, no pid file is created if none is specified in the configuration. When the proxy is daemonized, the pid file is used even if not specified, defaulting to "/var/run/redis-cluster-proxy.pid". Creating a pid file is best effort: if the proxy is not able to create it nothing bad happens, the server will start and run normally. pidfile /var/run/redis-cluster-proxy.pid 日志文件指定,如果可以正常启动,强烈建议指定一个输出日志文件,所有的运行异常或者错误都可以从日志中查找 Specify the log file name. Also the empty string can be used to force Redis Cluster Porxy to log on the standard output. Note that if you use standard output for logging but daemonize, logs will be sent to /dev/null logfile "" logfile "/usr/local/redis_cluster_proxy/redis_cluster_proxy.log" 跨slot操作,这里设置为yes,允许 Enable cross-slot queries that can use multiple keys belonging to different slots or even different nodes. WARN: these queries will break the the atomicity deisgn of many Redis commands. NOTE: cross-slots queries are not supported by all the commands, even if this feature is enabled enable-cross-slot no enable-cross-slot yes Maximum number of clients allowed max-clients 10000 连接到redis cluster时候的身份认证,如果redis集群节点设置了身份认证的话,强烈建议redis集群所有节点设置一个统一的auth Authentication password used to authenticate on the cluster in case its nodes are password-protected. The password will be used both for fetching cluster's configuration and to automatically authenticate proxy's internal connections to the cluster itself (both multiplexing shared connections and clients' private connections. So, clients connected to the proxy won't need to issue the Redis AUTH command in order to be authenticated. auth mypassw auth your_redis_cluster_password 这个节点是redis 6.0之后的用户名,这里没有指定 Authentication username (supported by Redis >= 6.0) auth-user myuser LOGGING Log level: can be debug, info, success, warning o error. log-level error Dump queries received from clients in the log (log-level debug required) dump-queries no Dump buffer in the log (log-level debug required) dump-buffer no Dump requests' queues (requests to send to cluster, request pending, ...) in the log (log-level debug required) dump-queues no 启动redis-cluster-proxy,./bin/redis-cluster-proxy -c ./proxy.conf需要注意的是,首次运行时直接打印出来启动日志或者异常信息,保证可以正常启动,然后再以daemonize方式运行因为笔者一开始遇到了一些错误,发现同样的错误,控制台直接打印出来的日志,跟daemonize方式运行打印到文件的日志不完全一致。 redis-cluster-proxy尝试与普通的redis 集群链接方式不同,redis-cluster-proxy模式下,客户端可以连接至redis-cluster-proxy节点,而无需知道Redis集群自身的详细信息,这里尝试执行一个multpile操作 这里使用传统的集群链接方式,来查看上面multiple操作的数据,可以发现的确是写入到集群中不同的节点中了。 故障转移测试简单粗暴地关闭一个主节点,这里直接关闭192.168.0.61节点,看看redis-cluster-proxy能否正常读写1,首先redis cluster自身的故障转移是没有问题的,完全成功 2,192.168.0.64接替192.168.0.61成为主节点 3,proxy节点操作数据卡死 查看redis-cluster-proxy的日志,说192.168.0.61节点无法连接,proxy失败失败退出 由此可见,正如日志里说明的,Redis Cluster Proxy v999.999.999 (unstable),期待有更稳定的版本推出。类似问题作者本人也有回应,参考:https://github.com/RedisLabs/redis-cluster-proxy/issues/36The Proxy currently requires that all nodes of the cluster must be up at startup when it fetches the cluster's internal map.I'll probably change this in the next weeks. redis-cluster-proxy是完美的解决方案?因为刚推出不久,生产环境基本上不会有太多实际的应用,里面肯定有不少坑,但不妨害对其有更多的期待。初次尝试可以感受的到,redis-cluster-proxy是一个非常轻量级,清爽简单的proxy代理层,它解决了一些redis cluster存在的一些实际问题,对应于程序来说也带来了一些方便性。如果没有源码开发能力,相比其他第三方proxy中间件,必须要承认官方可靠性和权威性。那么,redis-cluster-proxy是一个完美的解决方案么,留下两个问题1,如何解决redis-cluster-proxy单点故障?2,proxy节点的如何面对网络流量风暴? 原文地址https://www.cnblogs.com/wy123/p/12829673.html
接近8000字的Spring/SpringBoot常用注解总结!安排! 0.前言大家好,我是 Guide 哥!这是我的 221 篇优质原创文章。如需转载,请在文首注明地址,蟹蟹! 本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:https://github.com/Snailclimb/JavaGuide 相关阅读:V2.0 版本的 《JavaGuide面试突击版》来啦!带着它的在线阅读版本来啦! 可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了! 整个目录如下,内容有点多: 为什么要写这篇文章? 最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。 因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 哥感激不尽! 1. @SpringBootApplication这里先单独拎出@SpringBootApplication 注解说一下,虽然我们一般不会主动去使用它。 Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。 @SpringBootApplicationpublic class SpringSecurityJwtGuideApplication { public static void main(java.lang.String[] args) { SpringApplication.run(SpringSecurityJwtGuideApplication.class, args); } }我们可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。 package org.springframework.boot.autoconfigure;@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ......} package org.springframework.boot;@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic @interface SpringBootConfiguration { }根据 SpringBoot 官网,这三个注解的作用分别是: @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制@ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。@Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类 Spring Bean 相关2.1. @Autowired 自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。 @Servicepublic class UserService { ......} @RestController@RequestMapping("/users")public class UserController { @Autowired private UserService userService; ......}2.2. Component,@Repository,@Service, @Controller我们一般使用 @Autowired 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 @Autowired注解自动装配的 bean 的类,可以采用以下注解实现: @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。2.3. @RestController@RestController注解是@Controller和@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。 Guide 哥:现在都是前后端分离,说实话我已经很久没有用过@Controller。如果你的项目太老了的话,就当我没说。 单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller +@ResponseBody 返回 JSON 或 XML 形式数据 关于@RestController 和 @Controller的对比,请看这篇文章:@RestController vs @Controller。 2.4. @Scope声明 Spring Bean 的作用域,使用方法: @Bean@Scope("singleton")public Person personSingleton() { return new Person(); }四种常见的 Spring Bean 的作用域: singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。prototype : 每次请求都会创建一个新的 bean 实例。request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。2.5. Configuration一般用来声明配置类,可以使用 @Component注解替代,不过使用Configuration注解声明配置类更加语义化。 @Configurationpublic class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } } 处理常见的 HTTP 请求类型 种常见的请求类型: GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。3.1. GET 请求@GetMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.GET) @GetMapping("/users")public ResponseEntity> getAllUsers() { return userRepository.findAll();}3.2. POST 请求@PostMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.POST) 关于@RequestBody注解的使用,在下面的“前后端传值”这块会讲到。 @PostMapping("/users")public ResponseEntity createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { return userRespository.save(user);}3.3. PUT 请求@PutMapping("/users/{userId}") 等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT) @PutMapping("/users/{userId}")public ResponseEntity updateUser(@PathVariable(value = "userId") Long userId, @Valid @RequestBody UserUpdateRequest userUpdateRequest) { ......}3.4. DELETE 请求@DeleteMapping("/users/{userId}")等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE) @DeleteMapping("/users/{userId}")public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ ......}3.5. PATCH 请求一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。 @PatchMapping("/profile") public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) { studentRepository.updateDetail(studentUpdateRequest); return ResponseEntity.ok().build(); } 前后端传值掌握前后端传值的正确姿势,是你开始 CRUD 的第一步! 4.1. @PathVariable 和 @RequestParam@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。 举个简单的例子: @GetMapping("/klasses/{klassId}/teachers")public List getKlassRelatedTeachers( @PathVariable("klassId") Long klassId, @RequestParam(value = "type", required = false) String type ) { ...}如果我们请求的 url 是:/klasses/{123456}/teachers?type=web 那么我们服务获取到的数据就是:klassId=123456,type=web。 4.2. @RequestBody用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。 我用一个简单的例子来给演示一下基本使用! 我们有一个注册的接口: @PostMapping("/sign-up")public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) { userService.save(userRegisterRequest); return ResponseEntity.ok().build();}UserRegisterRequest对象: @Data@AllArgsConstructor@NoArgsConstructorpublic class UserRegisterRequest { @NotBlank private String userName; @NotBlank private String password; @FullName @NotBlank private String fullName; }我们发送 post 请求到这个接口,并且 body 携带 JSON 数据: {"userName":"coder","fullName":"shuangkou","password":"123456"}这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上。 需要注意的是:一个请求方法只可以有一个@RequestBody,但是可以有多个@RequestParam和@PathVariable。 如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你的数据库设计或者系统设计出问题了! 读取配置信息很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。 下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。 我们的数据源application.yml内容如下:: wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! my-profile: name: Guide哥 email: koushuangbwcx@163.com library: location: 湖北武汉加油中国加油 books: - name: 天才基本法 description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 - name: 时间的秩序 description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 - name: 了不起的我 description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? 5.1. @value(常用)使用 @Value("${property}") 读取比较简单的配置信息: @Value("${wuhan2020}")String wuhan2020;5.2. @ConfigurationProperties(常用)通过@ConfigurationProperties读取配置信息并与 bean 绑定。 @Component@ConfigurationProperties(prefix = "library")class LibraryProperties { @NotEmpty private String location; private List<Book> books; @Setter @Getter @ToString static class Book { String name; String description; } 省略getter/setter ......}你可以像使用普通的 Spring bean 一样,将其注入到类中使用。 5.3. PropertySource(不常用)@PropertySource读取指定 properties 文件 @Component@PropertySource("classpath:website.properties") class WebSite { @Value("${url}") private String url; 省略getter/setter ......}更多内容请查看我的这篇文章:《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》 。 参数校验数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。 JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便! 校验的时候我们实际用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。 SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成): 非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》。 需要注意的是: 所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints 6.1. 一些常用的字段验证的注解@NotEmpty 被注释的字符串的不能为 null 也不能为空@NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符@Null 被注释的元素必须为 null@NotNull 被注释的元素必须不为 null@AssertTrue 被注释的元素必须为 true@AssertFalse 被注释的元素必须为 false@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式@Email 被注释的元素必须是 Email 格式。@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值@Size(max=, min=)被注释的元素的大小必须在指定的范围内@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内@Past被注释的元素必须是一个过去的日期@Future 被注释的元素必须是一个将来的日期......6.2. 验证请求体(RequestBody)@Data@AllArgsConstructor@NoArgsConstructorpublic class Person { @NotNull(message = "classId 不能为空") private String classId; @Size(max = 33) @NotNull(message = "name 不能为空") private String name; @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围") @NotNull(message = "sex 不能为空") private String sex; @Email(message = "email 格式不正确") @NotNull(message = "email 不能为空") private String email; }我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。 @RestController@RequestMapping("/api")public class PersonController { @PostMapping("/person") public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) { return ResponseEntity.ok().body(person); } }6.3. 验证请求参数(Path Variables 和 Request Parameters)一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。 @RestController@RequestMapping("/api")@Validatedpublic class PersonController { @GetMapping("/person/{id}") public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { return ResponseEntity.ok().body(id); } }更多关于如何在 Spring 项目中进行参数校验的内容,请看《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》这篇文章。 全局处理 Controller 层异常介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。 相关注解: @ControllerAdvice :注解定义全局异常处理类@ExceptionHandler :注解声明异常处理方法如何使用呢?拿我们在第 5 节参数校验这块来举例子。如果方法参数不对的话就会抛出MethodArgumentNotValidException,我们来处理这个异常。 @ControllerAdvice@ResponseBodypublic class GlobalExceptionHandler { /** * 请求参数异常处理 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) { ...... } }更多关于 Spring Boot 异常处理的内容,请看我的这两篇文章: SpringBoot 处理异常的几种常见姿势使用枚举简单封装一个优雅的 Spring Boot 全局异常处理! JPA 相关8.1. 创建表 @Entity声明一个类对应一个数据库实体。 @Table 设置表明 @Entity@Table(name = "role")public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; 省略getter/setter...... }8.2. 创建主键@Id :声明一个字段为主键。 使用@Id声明之后,我们还需要定义主键的生成策略。我们可以使用 @GeneratedValue 指定主键生成策略。 1.通过 @GeneratedValue直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。 @Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;JPA 使用枚举定义了 4 中常见的主键生成策略,如下: Guide 哥:枚举替代常量的一种用法 public enum GenerationType { /** * 使用一个特定的数据库表格来保存主键 * 持久化引擎通过关系数据库的一张特定的表格来生成主键, */ TABLE, /** *在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键 */ SEQUENCE, /** * 主键自增长 */ IDENTITY, /** *把主键生成策略交给持久化引擎(persistence engine), *持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种 */ AUTO } @GeneratedValue注解默认使用的策略是GenerationType.AUTO public @interface GeneratedValue { GenerationType strategy() default AUTO; String generator() default ""; }一般使用 MySQL 数据库的话,使用GenerationType.IDENTITY策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。 2.通过 @GenericGenerator声明一个主键策略,然后 @GeneratedValue使用这个策略 @Id@GeneratedValue(generator = "IdentityIdGenerator")@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity")private Long id;等价于: @Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;jpa 提供的主键生成策略有如下几种: public class DefaultIdentifierGeneratorFactory implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService { @SuppressWarnings("deprecation") public DefaultIdentifierGeneratorFactory() { register( "uuid2", UUIDGenerator.class ); register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated register( "assigned", Assigned.class ); register( "identity", IdentityGenerator.class ); register( "select", SelectGenerator.class ); register( "sequence", SequenceStyleGenerator.class ); register( "seqhilo", SequenceHiLoGenerator.class ); register( "increment", IncrementGenerator.class ); register( "foreign", ForeignGenerator.class ); register( "sequence-identity", SequenceIdentityGenerator.class ); register( "enhanced-sequence", SequenceStyleGenerator.class ); register( "enhanced-table", TableGenerator.class ); } public void register(String strategy, Class generatorClass) { LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() ); final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass ); if ( previous != null ) { LOG.debugf( " - overriding [%s]", previous.getName() ); } } }8.3. 设置字段类型@Column 声明字段。 示例: 设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空 @Column(name = "user_name", nullable = false, length=32)private String userName;设置字段类型并且加默认值,这个还是挺常用的。 Column(columnDefinition = "tinyint(1) default 1")private Boolean enabled;8.4. 指定不持久化特定字段@Transient :声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。 如果我们想让secrect 这个字段不被持久化,可以使用 @Transient关键字声明。 Entity(name="USER")public class User { ...... @Transient private String secrect; // not persistent because of @Transient }除了 @Transient关键字声明, 还可以采用下面几种方法: static String secrect; // not persistent because of staticfinal String secrect = “Satish”; // not persistent because of finaltransient String secrect; // not persistent because of transient一般使用注解的方式比较多。 8.5. 声明大字段@Lob:声明某个字段为大字段。 @Lobprivate String content;更详细的声明: @Lob//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟 加载,而 FetchType. LAZY 表示延迟加载 ;@Basic(fetch = FetchType.EAGER)//columnDefinition 属性指定数据表对应的 Lob 字段类型@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")private String content;8.6. 创建枚举类型的字段可以使用枚举类型的字段,不过枚举字段要用@Enumerated注解修饰。 public enum Gender { MALE("男性"), FEMALE("女性"); private String value; Gender(String str){ value=str; } }@Entity@Table(name = "role")public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; @Enumerated(EnumType.STRING) private Gender gender; 省略getter/setter...... }数据库里面对应存储的是 MAIL/FEMAIL。 8.7. 增加审计功能只要继承了 AbstractAuditBase的类都会默认加上下面四个字段。 @Data@AllArgsConstructor@NoArgsConstructor@MappedSuperclass@EntityListeners(value = AuditingEntityListener.class)public abstract class AbstractAuditBase { @CreatedDate @Column(updatable = false) @JsonIgnore private Instant createdAt; @LastModifiedDate @JsonIgnore private Instant updatedAt; @CreatedBy @Column(updatable = false) @JsonIgnore private String createdBy; @LastModifiedBy @JsonIgnore private String updatedBy; } 我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目): @Configuration@EnableJpaAuditingpublic class AuditSecurityConfiguration { @Bean AuditorAware<String> auditorAware() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) .map(Authentication::getName); } }简单介绍一下上面设计到的一些注解: @CreatedDate: 表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值@CreatedBy :表示该字段为创建人,在这个实体被 insert 的时候,会设置值@LastModifiedDate、@LastModifiedBy同理。 @EnableJpaAuditing:开启 JPA 审计功能。 8.8. 删除/修改数据@Modifying 注解提示 JPA 该操作是修改操作,注意还要配合@Transactional注解使用。 @Repositorypublic interface UserRepository extends JpaRepository { @Modifying @Transactional(rollbackFor = Exception.class) void deleteByUserName(String userName); }8.9. 关联关系@OneToOne 声明一对一关系@OneToMany 声明一对多关系@ManyToOne声明多对一关系MangToMang声明多对多关系更多关于 Spring Boot JPA 的文章请看我的这篇文章:一文搞懂如何在 Spring Boot 正确中使用 JPA 。 事务 @Transactional在要开启事务的方法上使用@Transactional注解即可! @Transactional(rollbackFor = Exception.class)public void save() { ......} 我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。 @Transactional 注解一般用在可以作用在类或者方法上。 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public 方法都配置相同的事务属性信息。作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。更多关于关于 Spring 事务的内容请查看: 可能是最漂亮的 Spring 事务管理详解一口气说出 6 种 @Transactional 注解失效场景 json 数据处理10.1. 过滤 json 数据 @JsonIgnoreProperties 作用在类上用于过滤掉特定字段不返回或者不解析。 //生成json时将userRoles属性过滤@JsonIgnoreProperties({"userRoles"})public class User { private String userName; private String fullName; private String password; @JsonIgnore private List<UserRole> userRoles = new ArrayList<>(); }@JsonIgnore一般用于类的属性上,作用和上面的@JsonIgnoreProperties 一样。 public class User { private String userName; private String fullName; private String password; //生成json时将userRoles属性过滤 @JsonIgnore private List<UserRole> userRoles = new ArrayList<>(); }10.2. 格式化 json 数据@JsonFormat一般用来格式化 json 数据。: 比如: @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")private Date date;10.3. 扁平化对象@Getter@Setter@ToStringpublic class Account { @JsonUnwrapped private Location location; @JsonUnwrapped private PersonInfo personInfo; @Getter @Setter @ToString public static class Location { private String provinceName; private String countyName; } @Getter @Setter @ToString public static class PersonInfo { private String userName; private String fullName; }} 未扁平化之前: { "location": { "provinceName":"湖北", "countyName":"武汉" }, "personInfo": { "userName": "coder1234", "fullName": "shaungkou" } }使用@JsonUnwrapped 扁平对象之后: @Getter@Setter@ToStringpublic class Account { @JsonUnwrapped private Location location; @JsonUnwrapped private PersonInfo personInfo; ...... }{ "provinceName":"湖北", "countyName":"武汉", "userName": "coder1234", "fullName": "shaungkou"} 测试相关@ActiveProfiles一般作用于测试类上, 用于声明生效的 Spring 配置文件。 @SpringBootTest(webEnvironment = RANDOM_PORT)@ActiveProfiles("test")@Slf4jpublic abstract class TestBase { ......}@Test声明一个方法为测试方法 @Transactional被声明的测试方法的数据会回滚,避免污染测试数据。 @WithMockUser Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。 @Test @Transactional @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER") void should_import_student_success() throws Exception { ...... } 暂时总结到这里吧!虽然花了挺长时间才写完,不过可能还是会一些常用的注解的被漏掉,所以,我将文章也同步到了 Github 上去,Github 地址:https://github.com/Snailclimb/JavaGuide/blob/master/docs/system-design/framework/spring/spring-annotations.md 欢迎完善! 本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:https://github.com/Snailclimb/JavaGuide。 原文地址https://www.cnblogs.com/javaguide/p/spring-annotations.html
开发机直连 Docker 中的 Redis 容器小教程 在笔者日常开发中,都是把redis装在windows系统中。虽然可以通过RedisDesktopManager等客户端工具连接操作redis,但是还是觉得low了一些。因为作为程序员,我可能更想在Linux系统操作redis,这样在遇到生产环境实操时候,才不会显得束手无策。 今天它来了,我们将会在虚机中安装docker,然后在docker中安装redis,最后让我们宿主机(开发机)连接到我们安装的redis,测试能否正常使用。 因为在win7中安装虚机不是我们这篇文章主要目的,大家可以自己搜索资料安装。 一.你需要准备什么?windows7VMware Workstation ProCentOS 7.0MobaXterm_PersonalDocker CE 支持 64 位版本 CentOS 7,并且要求内核版本不低于 3.10。 CentOS 7 满足最低内核的要求,但由于内核版本比较低,部分功能(如 overlay2 存储层驱动)无法使用,并且部分功能可能不太稳定。 其中,我是用前三个搭建Linux工作环境,用MobaXterm_Personal工具连接操作Linux系统.如果你有一套可操作的Linux工作环境,完全可以跳过这一步; 二.安装Docker2.1 卸载旧版本 $ sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine 2.4 安装 yum执行以下命令安装依赖包: $ sudo yum install -y yum-utils device-mapper-persistent-data lvm2 鉴于国内网络问题,强烈建议使用国内源,我们选用中科大镜像源 执行下面的命令添加 yum 软件源:放置一些类似docker软件的仓库 关于yum源,可以阅读这篇文章yum源解释: https://blog.csdn.net/qq_41869566/article/details/79945078 $ sudo yum-config-manager --add-repo https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo 2.5 安装docker$ sudo yum install -y docker-ce docker-ce-cli containerd.io 2.6 启动docker$ sudo systemctl start docker 2.7 配置docker国内镜像加速国内从 Docker Hub 拉取镜像有时会遇到困难,此时可以配置镜像加速器。国内很多云服务商都提供了国内加速器服务,例如: 网易云加速器 https://hub-mirror.c.163.com阿里云加速器(需登录账号获取)由于镜像服务可能出现宕机,建议同时配置多个镜像。各个镜像站测试结果请到 docker-practice/docker-registry-cn-mirror-test 查看。 国内各大云服务商均提供了 Docker 镜像加速服务,建议根据运行 Docker 的云平台选择对应的镜像加速服务,具体请参考官方文档。 本节我们以 网易云 镜像服务 https://hub-mirror.c.163.com 为例进行介绍。 Ubuntu 16.04+、Debian 8+、CentOS 7请在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在请新建该文件) { "registry-mirrors": [ "https://hub-mirror.c.163.com" ]} 注意,一定要保证该文件符合 json 规范,否则 Docker 将不能启动。 之后重新启动服务。 $ sudo systemctl daemon-reload$ sudo systemctl restart docker 2.8 测试docker$ sudo docker run hello-world 三 安装redis3.1 拉取镜像$ sudo docker pull redis 查看镜像 $ sudo docker images 3.2 准备目录 #进入用户根目录 $ sudo cd ~ #创建文件夹 $ sudo mkdir -p /root/Downloads/redis/conf $ sudo mkdir -p /root/Downloads/redis/data #进入到conf目录 $ sudo cd /root/redis/redis01/conf #下载一个redis.conf文件[如果太慢,直接网页下载上传到服务器目录] $ sudo wget http://download.redis.io/redis-stable/redis.conf 3.3 启动redis因为默认镜像没有配置文件,要是宿主主机连接,我们要挂载配置文件目录 进入这个目录 $ sudo cd /root/Downloads/redis 启动redis容器 $ sudo docker run -p 6379:6379 --privileged=true --name redis -v $PWD/conf/redis.conf:/etc/redis/redis.conf -v $PWD/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes 3.4 查看正在运行的容器 $ sudo docker container ps 3.5 测试redis是否正常 // 查看正在运行的容器$ sudo docker ps //进入容器内部$ sudo docker exec -it /bin/bash //退出ctrl + D 3.6 修改redis配置文件vim基本操作 本来 bind 127.0.0.1protected-mode yes 更改为 bind 127.0.0.1 protected-mode norequirepass milo 重启redis容器 四.宿主主机连接redis4.1 客户端工具连接测试首先,查看工具连接linux的ip,笔者的如下: 使用RedisDesktopManager工具测试 4.2 开发机连接测试首先,我们修改配置文件中redis的连接ip,然后启动项目,访问一个带有缓存的页面,我们去redis容器中看看是否有指定key 五.总结经过上面的一顿操作,我们以后开发机直接连接redis容器,也能多熟悉linux系统,谢谢大家阅读 原文地址https://www.cnblogs.com/javazhiyin/p/12802529.html
【Asp.NetCore源码】设计模式 - 提供者模式 AspNetCore源代码发现日志模块的设计模式(提供者模式),特此记录 学习设计模式的好处是,我们可以容易扩展它达到我们要求,除了要知道如何扩展它,还应该在其他地方应用它 类图 & 分析 角色分析 日志工厂 ( LoggerFactory --> ILoggerFactory) 提供注册提供者 创建日志记录器(Logger) 日志记录器(Logger --> ILogger) 写入日志记录(遍历所有日志提供者的Logger) 这里所有注册的日志提供者聚合 日志提供者(ConsoleLoggerProvider --> ILoggerProvider) 创建具体日志记录器 具体日志记录者(ConsoleLogger,EventLogLogger) 将日志写入具体媒介(控制台,Windows事件日志) 现在来看看这个模式 提供标准的日志写入接口(ILogger) 提供日志提供者接口(ILoggerProvider) 提供注册提供者接口(ILoggerFactory.AddProvider) 这里只是列出部分类和方法,整个Logging要比这个还多,为什么写个日志要整那么多东西? 程序唯一不会变就是不断在变化,这个也是为什么要设计模式运用到程序当中的原因,让程序可扩展来应对这种变化。 AspNetCore内置 8种日志记录提供程序 ,但肯定还是远远不够,因为有的可能想把日志写在文本,有的想写在Mongodb,有的想写在ElasticSearch等等,Microsoft不可能把所有的都实现,就算实现也未必适合你的业务使用。 假设现在需要把日志写在Mongo,只需要 实现Mongodb的ILogger - 将日志写到Mongodb 实现Mongodb的ILoggerProvider - 创建Mongodb的Logger 把Provider注册到AspNetCore - ILoggerFactory.AddProvider 这里都是新增代码达到实现把日志写入到Mongodb,这就是6大设计原则之一对扩展开放(可以添加自己的日志),对修改封闭(不需要修改到内部的方法) AspNetCore代码实现(只列出接口) ILoggerFactory ILogger CreateLogger(string categoryName);void AddProvider(ILoggerProvider provider);CreateLogger : 这个和ILoggerProvider提供的CreateLogger虽然都是现实ILogger接口,但是做的事情不一样,LoggerFactory创建的是Logger实例,里面聚合了具体写日志的Logger,遍历它们输出。 categoryName : 可以指定具体,若使用泛型相当于typeof(T).FullName,这个用于筛选过滤日志 AddProvider : 注册一个新的提供者,然后遍历现有的Logger,把新的Provider添加到现有logger里面 ILoggerProvider ILogger CreateLogger(string categoryName);CreateLogger : 用于创建具体写日志Logger(例如Console) ILogger void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter);bool IsEnabled(LogLevel logLevel);IDisposable BeginScope(TState state);Log(....): 输出日志 bool IsEnabled : 指定的日志级别是否可用 IDisposable BeginScope() : 开启日志作用域,将这个域范围的日志都放一起 AspNetCore使用第三方日志组件(Log4Net) AspNetCore使用Log4Net作为记录很简单,只需 安装包: dotnet install Microsoft.Extensions.Logging.Log4Net.AspNetCore Configure 添加: loggerFactory.AddLog4Net(); 添加log4net.config配置文件 看看Microsoft.Extensions.Logging.Log4Net.AspNetCore如何实现ILogger和ILoggerProvider接口(代码有截取) Log4NetProvider public ILogger CreateLogger(string categoryName) => this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation); private Log4NetLogger CreateLoggerImplementation(string name){ var options = new Log4NetProviderOptions { Name = name, LoggerRepository = this.loggerRepository.Name }; options.ScopeFactory = new Log4NetScopeFactory(new Log4NetScopeRegistry()); return new Log4NetLogger(options); } Log4NetLogger switch (logLevel){ case LogLevel.None: break; case LogLevel.Critical: { string overrideCriticalLevelWith = options.OverrideCriticalLevelWith; if (!string.IsNullOrEmpty(overrideCriticalLevelWith) && overrideCriticalLevelWith.Equals(LogLevel.Critical.ToString(), StringComparison.OrdinalIgnoreCase)) { log.Critical(text, exception); } else { log.Fatal(text, exception); } break; } case LogLevel.Debug: log.Debug(text, exception); break; case LogLevel.Error: log.Error(text, exception); break; ...... } log4net的ILog是没有Trace和Critical方法,这两个是扩展方法,调用log4net log4net.Repository.Hierarchy.Logger.Log()方法 log4net 里面有Fatal代表日志最高级别,AspNetCore的Critical是日志最高级别,习惯log4net可能习惯用Fatal,这个时候只需要在注册的时候 loggerFactory.AddLog4Net(new Log4NetProviderOptions(){ OverrideCriticalLevelWith = "Critical" });在Controller调用 _logger.LogCritical("Log Critical");看看效果 2020-04-27 13:42:05,042 [10] FATAL LoggingPattern.Controllers.WeatherForecastController (null) - Log Critical 奇怪,没有按预期发生。这个组件是开源的,可以下载下来调试看看,github克隆下来 Microsoft.Extensions.Logging.Log4Net.AspNetCore 调试过程 将Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj的SignAssembly设置false(这个是程序集强签名) false 2. 将引用改成引用本地,我这里是放在跟项目平级 <ProjectReference Include="..\Microsoft.Extensions.Logging.Log4Net.AspNetCore\src\Microsoft.Extensions.Logging.Log4Net.AspNetCore\Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj" /> 我这里是用VSCode,如果用VS不用这么麻烦 然后就可以打断点,在写日志和之前看到的那个判断打个断点 接下来就是看看这个值怎么来的 builder.Services.AddSingleton(new Log4NetProvider(options)); public Log4NetProvider(Log4NetProviderOptions options){} 注册一个单例的Log4NetProvider,参入参数options,Logger是在Provider的CreateLogger创建,现在看看CreateLogger public ILogger CreateLogger(string categoryName) => this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation); private Log4NetLogger CreateLoggerImplementation(string name){ var options = new Log4NetProviderOptions { Name = name, LoggerRepository = this.loggerRepository.Name }; options.ScopeFactory = new Log4NetScopeFactory(new Log4NetScopeRegistry()); return new Log4NetLogger(options); } 到这里就清楚了,CreateLoggerImplementation里面又new了一个options,然后没有给OverrideCriticalLevelWith赋值(我认为这是个Bug,应该也很少人会用这个功能)这里之所以没用单例的options,因为要给每个Logger的目录名称动态赋值。 给这个库作者提了Issues和PR 添加自定义的日志记录器 假设现在需要把日志加入到Mongodb,只需完成下面几个步骤 添加Mongodb驱动,(dotnet-cli) dotnet add package MongoDB.Driver 实现接口ILogger public class MongodbLogger : ILogger{ private readonly string _name; private MongoDB.Driver.IMongoDatabase _database; public MongodbLogger(string name, MongoDB.Driver.IMongoDatabase database) { _name = name; _database = database; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { var collection = _database.GetCollection<dynamic>(logLevel.ToString().ToLower()); string message = formatter(state, exception); collection.InsertOneAsync(new { time = DateTime.Now, name = _name, message, exception }); } public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; public System.IDisposable BeginScope<TState>(TState state) => NullScope.Instance; } 实现ILoggerProvider接口 public class MongodbProvider : ILoggerProvider{ private readonly ConcurrentDictionary<string, MongodbLogger> _loggers = new ConcurrentDictionary<string, MongodbLogger>(); private MongoDB.Driver.IMongoDatabase _database; public MongodbProvider(MongoDB.Driver.IMongoDatabase database) { _database = database; } public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd(categoryName, name => new MongodbLogger(categoryName, this._database)); public void Dispose() => this._loggers.Clear(); } 添加MongodbLogging扩展函数(非必须) public static ILoggerFactory AddMongodb(this ILoggerFactory factory, string connetionString = "mongodb://127.0.0.1:27017/logging"){ var mongoUrl = new MongoDB.Driver.MongoUrl(connetionString); var client = new MongoDB.Driver.MongoClient(mongoUrl); factory.AddProvider(new MongodbProvider(client.GetDatabase(mongoUrl.DatabaseName))); return factory; } Configure注册MongodbLogging loggerFactory.AddMongodb();运行效果 扩展 设计模式的好处是,我们可以容易扩展它达到我们要求,除了要知道如何扩展它,还应该在其他地方应用它,例如我们经常需要消息通知用户,但是通知渠道(提供者),在一开始未必全部知道,例如一开始只有短信,邮件通知,随着业务发展可能需要增加微信推送,提供者模式就很好应对这一种情况,很容易画出下面类图。 当需要扩展发送消息渠道,只需要实现ISenderProvider(哪个提供),ISender(如何发送) 当需要扩展发送消息渠道,只需要实现ISenderProvider(哪个提供),ISender(如何发送) 转发请标明出处:https://www.cnblogs.com/WilsonPan/p/12793220.html 示例代码:https://github.com/WilsonPan/AspNetCoreExamples/tree/master/LoggingPattern
44道JavaScript送命题 很久以前看过一个老外写的帖子,JavaScript Puzzlers!,直译就是JavaScript难题,里面列举了44道JavaScript选择题,大部分都是让人摸不着头脑的题目,需要仔细琢磨一番才能得到正确答案。也有一些作者也没有解释清除,直接通过实验给出答案了。 这44个问题是在ECMA 262(5.1)环境下,浏览器中试验的,如果是node环境下可能不同。这是因为二者环境差异,比如node环境下顶层变量是global,浏览器环境下则是windows。 本文部分内容也参考了文章Javascript 变态题解析。 map&parseInt传参["1", "2", "3"].map(parseInt)结果是什么? map方法指定一个回调函数,重新创建一个由回调函数返回值组成的新数组。该方法的原型是: var new_array = arr.map(function callback(currentValue[, index[, array]]) {// Return element for new_array }[, thisArg])map接受2个参数,一个是回调函数callback,一个是回调函数的this值。 解释如下: callback:生成新数组元素的函数,有三个参数currentValue:callbac当前正在处理的元素index:callback当前正在处理的当前元素的索引array:map方法调用的数组本身thisArg:执行callback函数时值被当做thisNumber.parseInt接受两个参数,原型Number.parseInt(string[, radix]),一个是要解析的值,一般是字符串,如果不是的话,使用toString方法将它转化为字符串。参数radix,是一个介于2到36之间的整数,如果省略该值或者为0,则按照10进制来解析,也就是说默认值是10,如果是“0x”或者“0X”开头,则以16进制为基数。如果小于2或者大于36,则parseInt返回NaN。 也就是说[].map(parseInt)这种写法根本就是想当然的,本题相当于下面的三句: parseInt('1', 0);parseInt('2', 1);parseInt('3', 2); 这三句只有第一句会把第二个参数0默认为10,剩下两句都不满足radix参数介于2到36之间,所有返回[1, NaN, NaN]。另外,如果想得到正确的结果,应该这样写["1", "2", "2"].map(i => parseInt(i))。 typeof和instanceof运行[typeof null, null instanceof Object]这个表达式结果是什么?,这个主要考察typeof,instanceof两个操作符,前者是返回一个字符串表示未经计算的操作数的类型,后者是判断null的原型链中是否出现了Object的构造函数的property。 这两个操作符用来判断类型,前者常用来判断字面量,后者用来判断对象的类型,但是两个都有缺陷,详见另一篇文章《javascript中判断数据类型》 但是null是一个比较特殊的值,type of null返回的是“objec”,这是因为JavaScript最初实现中,值是由一个表示类型的标签和实际数值表示的。对象的类型标签是0,由于null代表是空指针(大多数平台下值为0x00),因此null的类型标签是0,typeof null也就返回“object”。 null是所有原型链的最顶端,null instanceof Object返回false,假设null这个值有构造函数Null,obj instanceof Null才会返回true。 所以上面表达式返回["object", false]。 3.reduce&Math.pow传参表达式[ [3,2,1].reduce(Math.pow), [].reduce(Math.pow) ]返回什么? 和第1题有些类似。arr.reduce方法对数组中每个元素执行一个自定义的reducer函数(升序执行),并将结果汇总为单个值,这个常常用来累加一个数组中的数字。原型如下: arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])callback:数组中每个值要执行的函数,有四个参数。accumulator:它是上一次回调函数时得到的值,或者initialValue。currentValue:数组中正在处理的当前元素。index:可选,数组中正在处理的元素的索引,如果提供了initialValue,则起始索引号为0,否则从索引1开始。array:调用reduce()的数组initialValue:作为第一次调用callback时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用reduce将报错。注意:如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。 回调函数第一次执行时,accumulator 和currentValue的取值有两种情况: 如果调用reduce()时提供了initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;如果没有提供 initialValue,那么accumulator取数组中的第一个值,currentValue取数组中的第二个值。如果数组为空且没有提供initialValue,会抛出TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供initialValue, 或者有提供initialValue但是数组为空,那么此唯一值将被返回并且callback不会被执行。Math.pow(base, exponent),返回基数base的指数exponent次幂,即baseexponent。 上面表达式调用reduce方法的时候没有提供initialValue,从索引1开始执行,第一次执行的时候accumulator取arr[0],这里是3,currentValue取第二个值,这里是2,传给Math.pow,得到9。 第二个表达式是在空数组上调用reduce,并且没有提供initialValue,所以抛出错误:VM146:1 Uncaught TypeError: Reduce of empty array with no initial value at Array.reduce ()。 最后整个表达式的结果还是抛出错误。 优先级 val = 'smtg'; console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');上面的表达式输出结果是什么?这个问题考察的是加号和三元运算的优先级,由于加号的优先级高于三元表达式,所以实际执行的是: console.log('Value is true' ? 'Something' : 'Nothing');因此最后输出“Something”。 5.变量提升问题 var name = 'World!';(function () { if (typeof name === 'undefined') { var name = 'Jack'; console.log('Goodbye ' + name); } else { console.log('Hello ' + name); } })(); 上面表达式输出什么? 这个是考察var变量提升问题,使用var申明的变量会提神到函数顶部,但是并不会初始化,这个是JavaScript内部机制。于是上面语句相当于: var name = 'World!';(function () { var name = undefined; if (typeof name === 'undefined') { var name = 'Jack'; console.log('Goodbye ' + name); } else { console.log('Hello ' + name); } })(); name的声明放在了函数顶部,但是值是undefined。因为代码又放在一个闭包里,用外层那个name = “world”是不能访问的,闭包有隔离变量的作用。最后,上面的语句输出“Goodbye Jack”。 JavaScript能表示的最大数 var END = Math.pow(2, 53);var START = END - 100;var count = 0;for (var i = START; i <= END; i++) { count++; }console.log(count); 上面表达式输出什么? 乍一看是100,其实是干扰,这考察的不是循环,var变量啥的,而是JavaScript能表示的最大的数字是253,即次幂表达式Math.pow(2, 53)。在这个最大数的基础上加上一个整数得到的结果都是不准确的。看下面的例子: var END = Math.pow(2, 53); var START = END - 5; var count = 0; console.log(END); // 9007199254740992 console.log(END + 7); // 9007199254740997 console.log(START++ <= END, START); // true 9007199254740988 console.log(START++ <= END, START); // true 9007199254740989 console.log(START++ <= END, START); // true 9007199254740990 console.log(START++ <= END, START); // true 9007199254740991 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 console.log(START++ <= END, START); // true 9007199254740992 缩小了演示范围,END已经是最大值9007199254740992,END+7,应该得到9007199254740998,其实是得到9007199254740997,这是一个错误的计算结果。如果在START的基础上累加(每次加1),到第五次(包含第五次)得到的结果都是9007199254740992。所以在原题的i++过程中,运行到第100次之后每次得到的值都是9007199254740992,都满足i<=END,也就是说这是一个死循环,程序一直运行,得不到任何结果。 7. 稀疏数组问题var ary = [0,1,2];ary[10] = 10;var result = ary.filter(function(x) { return x === undefined;});这个表达式的结果是什么? 这个问题考察的是稀疏数组,稀疏数组中的未赋值的元素是空,并且filter会忽略这些元素,所以上面filter语句返回的是空数组[]。使用console.log()语句输出稀疏数组如下,可以看到console.log()语句也会忽略掉空位。 所以本题输出的结果是[]。 8. 数字精度问题var two = 0.2;var one = 0.1;var eight = 0.8;var six = 0.6;console.log([two - one === one, eight - six === two])上面表达式输出结果是什么? 这个考察的是JavaScript数字精度问题,JavaScript的Number类型为双精度IEEE 754 64位浮点类型,0.1不能精确的转换成二进制,会有溢出,遵循IEEE 754标准的语言都有这个毛病,包括java。这个问题造成0.1 + 1.2!= 0.3,0.8 - 0.6 != 0.2等等。这里先这样简单解释一下,下次专门写一篇来解释这个问题。 本题输出结果是[true, false]。 9. 字面量问题 function showCase(value) { switch(value) { case 'A': console.log('Case A'); break; case 'B': console.log('Case B'); break; case undefined: console.log('undefined'); break; default: console.log('Do not know!'); } }showCase(new String('A')); 上面的语句输出什么? 这个其实考察的是字面量和对象是否恒相等的问题,case语句是使用恒等(===)来判断的,而‘A’ !== new Strting('A')返回false,所以最后输出‘Do not know’。判断恒等是三个等号'A' == new String('A')返回true,而两个等号会调用对象的toString()方法,'A'==new String('A')返回true,如下图: String()函数 function showCase2(value) { switch(value) { case 'A': console.log('Case A'); break; case 'B': console.log('Case B'); break; case undefined: console.log('undefined'); break; default: console.log('Do not know!'); } }showCase2(String('A')); 上面语句输出什么?String("A")不会创建一个对象,调用String方法返回一个字符串"A",所以输出“Case A”。 除法运算符% function isOdd(num) { return num % 2 == 1; }function isEven(num) { return num % 2 == 0; }function isSane(num) { return isEven(num) || isOdd(num); }var values = [7, 4, '13', -9, Infinity];values.map(isSane); 上面的map结果是什么? 这个题目是考察求余运算符,前两个数字7,4没什么好说的。‘13’%2,会把字符串转换成整形参与计算,得到1;-9%2会保留负号得到-1,符号是和第一个操作数保持一致,第二个操作数的符号会忽略,所以返回false,Infinity参与计求余计算得到NaN。所以最终结果是[true, true, true, false, false]。 parseIntconsole.log(parseInt(3, 8)); console.log(parseInt(3, 2));console.log(parseInt(3, 0));上面表达式分别输出什么? 这里和第一题一样,再一次考察parseInt这个函数,把题目翻译翻译是问:把8进制里的3转化成10进制整数是3;把2进制中的3转换成10进制整形得到NaN,因为2进制中没有3;把10进制中的3转换成10进制整数还是3;所以最终结果是[3, NaN, 3]。 Array.prototypeconsole.log(Array.isArray(Array.prototype)); 上面表达式输出结果是什么? 这个是是一个非常容易忽略的知识点Array.property,是Array函数的原型对象,还是一个数组,从下面的语句可以看出: 所以本题的答案是true。 if语句 var a = [0];if ([0]) { console.log(a == true);} else { console.log("wut");} 上面这个语句得到什么? 在JavaScript中if语句比较特殊,引用MDN中的介绍: 任何一个值,只要它不是 undefined、null、 0、NaN或空字符串(""),那么无论是任何对象,即使是值为假的Boolean对象,在条件语句中都为真。 这个特性给我们写if语句带来很大的便利,不需要考虑if语句中的变量类型,因为只要它不是上述的几种“没有意义”的值,判断都能通过,都能执行if语句。 但是非严格相等就不是这回事了,用==比较一个数组和true肯定得到false,所以本题结果是输出false。 对象比较问题[]==[] 上面语句的输出结果是什么? 这个考察的是对象比较问题,[]是一个空数组,数组属于对象,对象比较无论如何比较都不相等。如下图: 上面比较对象(复杂数据)得到的结果都是false,所以本题结果是false。 +是字符串连接符也是加法运算console.log('5' + 3); console.log('5' - 3);加号遇到字符串的时候就是字符串连接符,会调用toString方法把另一个非字符串转换成字符串,然后来连接,所以第一个结果是字符串‘53’;第二个是减号,它的行为和加号刚好相反,它是把字符串转换成整形,第二个输出2。 所以本题输出['53', 2]。 加减运算和正负运算console.log(1 + - + + + - + 1); 上面表达式输出什么结果? 这个要搞明白这个表达式其实是1 + (-+++-+1),除了第一个+是加法,后面的+,-都是正负运算,根据正正得正,正负得负,负负得正的原则-+++-+1是1,所以最后结果得到2。 还是稀疏数组 ary = Array(3); ary[0]=2let s = ary.map(function(elem) { return '1'; });console.log(s);上面语句输出什么? 这又是考察稀疏数组问题,map会忽略调稀疏数组中的空元素,输出结果是["1", empty × 2]。 argument对象 function sidEffecting(ary) { arguments[1] = 10; ary[0] = ary[2]; }function bar(a,b,c) { c = 10 sidEffecting(arguments); return a + b + c; }console.log(bar(1,1,1)) 上面语句输出什么? 这一题考察的是对argument对象的了解,argument是一个类数组对象,修改对象的属性值会影响其他使用到对象的地方,即使变量不在同一范围内。再加上对象属性可以使用类似数组下标的方式来访问,对象做了字符串到值的映射,而数组做的是数字到值的映射。 根据这些可以推导结果是21。 20. JavaScript中的最大数var a = 111111111111111110000, b = 1111; console.log(a + b);上面的语句输出什么内容? 这个又一次考察JavaScript中的最大值问题,JavaScript中最大值是Math.pow(2, 53)=9007199254740992,计算过程中如果超出这个最大值范围就不准确了,但是怎么个不准确法,是不确定的。这里输出结果是111111111111111110000, 还是a的值。 21. Array.property.reversevar x = [].reverse;x();上面语句输出什么? 这个有些奇怪了,原文中解释说reverse方法会返回调用这个方法的数组本身(就是this),但是x()没有调用者,所以this指向了全局对象window。但是我在chrome中试过,这个是会报错的,报错信息是:Uncaught TypeError: Cannot convert undefined or null to object at reverse ()。这个可能是原文写的比较早,后面的浏览器修改了这个行为。 22. Number.MIN_VALUEconsole.log(Number.MIN_VALUE > 0);这个考察Number.MIIN_VALUE的值,MDN上解释如下: Number.MIN_VALUE表示最小正数,即最接近 0 的正数 (实际上不会变成 0)。最大的负数是 -MIN_VALUE。 所以Number.MIN_VALUE是大于0的,这里输出是true。 23. 强制转换console.log([1 < 2 < 3, 3 < 2 < 1]);上面表达式输出什么? 这里考察的是在大于号,小于号运算中true会被强制转换为1,false会被强制转换成0。相当于console.log([true < 3, false < 1]),转换后就是console.log([1 < 3, 0 < 1]),所以这里输出结果是[true, true]。 24. 数组字面量的字符串表示console.log(2 == [[[2]]]);上面语句输出什么? 这个题套路深,我先说答案,是true。连原文作者都惊叹这是什么鬼?原文是:the most classic wtf!如果试着解释的话,非严格相等==在遇到字面量[[[2]]]的时候,会视图将它转换成字符串然后和2比较,但是[[[2]]].toString()返回‘2’,然后‘2’ == 2返回true,是不是很惊喜?是不是想骂人?如下图,这样非严格相等就返回true了。 3.和.3console.log(3.toString()); console.log(3..toString());console.log(3...toString());上面语句输出什么结果? 这里要搞清楚3.和.3都是合法的数字3.是一个省略了尾数部分0的数字,.3是一个省略了整数部分0的数字;第一句中但是toString()不是一个数字,所以第一句报错:Uncaught SyntaxError: Invalid or unexpected token。第二句相当于(3.).toString()输出”3“。第三句和第一句一样报错,原因也是一样的,第二个.后面的一串..toString()不是一个合法的数字,于是就报错了。 所以正确的答案是error,”3“,error。 26. var和闭包问题(function(){ var x = y = 1; })();console.log(y);console.log(x);上面的语句输出什么? 这一题考察的是对闭包和var变量的了解。闭包有隔离变量的作用,所以var不能提升变量,在闭包外部访问x是失败的,所以第二句输出Undefined。闭包中如果不使用var声明变量,直接不带var,这样申明y=1,反而y是全局的,在外部是可以访问到变量y的,所以第一句输出1。是不是很惊喜? 27. 正则表达式不可相互比较var a = /123/, b = /123/;console.log(a == b);console.log(a === b);上面两句分别输出什么? 这个考察的是正则表达式比较问题,虽然字面量内容相同,但是JavaScript认为这是两个正则表达式,是对象类型,他们是不相等的。这里输出结果为false false。 28. 数组比较 var a = [1, 2, 3], b = [1, 2, 3], c = [1, 2, 4]; console.log(a == b); console.log(a === b); console.log(a > c); console.log(a < c); 上面比较以此输出什么? 即使数组字面量相等,使用==或者===判断两个数组也不相等。而大于,小于比较是按照字典顺序比较的,这里以a 比较a[0]比较a[1]比较a[2]所以上面以此输出false,false,false,true。 29. 构造函数的原型var a = {}, b = Object.prototypeconsole.log([a.prototype === b, Object.getPrototypeOf(a) === b]);上面代码输出什么结果? JavaScript中函数才有prototype属性,对象是没有的,对象有__proto__,指向对象的构造函数的原型,所以a.prototype是undefined。b是Object函数的原型,是一个对象,所以第一个是false。第二个是使用getPrototypeOf方法获取对象a的原型(即a.__proto__),这个和Object.prototype相等的,所以第二个表达式返回true。 a.__proto__是对象的原型,它是浏览器暴露出来的属性,JavaScript标准中没有这个属性。下面的语句输出[true]: var a = {}, b = Object.prototypeconsole.log([a.__proto__ === b);如下图: 函数的原型对象 f() {} var a = f.prototype, b = Object.getPrototypeOf(f);console.log(a === b);上面的语句输出什么? 这还是考察函数原型问题。这f.prototype是获取函数f的原型对象,Object.getPrototypeOf(f)是获取对象构造函数的原型,注意函数也是对象,它的构造函数是Function(),因此f的构造函数的原型是Function.property,因此这这里输出false。 Function.name foo() { } var oldName = foo.name;foo.name = "bar";console.log([oldName, foo.name]);上面的语句输出什么结果? 这道题考察的是函数的name属性,function.name 属性返回函数实例的名称,这个属性是不可写的,因为访问器属性中writable属性为false。所以这一题输出的是['foo', 'foo']。 str.replaceconsole.log("1 2 3".replace(/d/g, parseInt)); 上面语句输出什么内容?这一题考察的是replace方法,str.replace方法的原型如下: str.replace(regexp|substr, newSubStr|function)参数解释如下: regexp (pattern):一个正则表达式对象(即RegExp对象)或者其字面量。该正则所匹配的内容会被第二个参数的返回值替换掉。它和substr是二选一的。substr (pattern):一个将被newSubStr替换的字符串。其被视为一整个字符串,而不是一个正则表达式。仅第一个匹配项会被替换。它和regexp是二选一的。newSubStr (replacement):用于替换掉第一个参数在原字符串中的匹配部分的字符串。该字符串中可以内插一些特殊的变量名。参考下面的使用字符串作为参数。它和function是二选一的。变量名 代表的值$$ 插入一个 "$"。$& 插入匹配的子串。$` 插入当前匹配的子串左边的内容。$' 插入当前匹配的子串右边的内容。$n 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始function (replacement):一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考下面的指定一个函数作为参数。它和newSubStr是二选一的。变量名 代表的值match 匹配的子串。(对应于上述的$&。)p1,p2, ... 假如replace()方法的第一个参数是一个RegExp 对象,则代表第n个括号匹配的字符串。(对应于上述的1,1,2等。)例如,如果是用 /(a+)(b+)/ 这个来匹配,p1 就是匹配的 a+,p2 就是匹配的 b+。offset 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 'abcd',匹配到的子字符串是 'bc',那么这个参数将会是 1)string 被匹配的原字符串。NamedCaptureGroup 命名捕获组匹配的对象不得不说replace方法有点复杂,在这个例子中第一个参数是一个正则表达式,第二个参数是一个函数,函数参数以此是match,offset,string,NamedCaptureGroup,但是parseInt仅仅接受2个参数,所以相当于执行下面的语句: parseInt('1', 0) // 10进制里的1是1parseInt('2', 2) // 2进制里没有2,所以NaNparseInt('3', 4) // 4进制中的3是3 所以最终结果是"1 NaN 3",这个作者很喜欢拿parseInt说事。 33. eval函数 function f() {}var parent = Object.getPrototypeOf(f);console.log(f.name);console.log(parent.name);console.log(typeof eval(f.name));console.log(typeof eval(parent.name)); 上面的语句输出什么? 第一句f.name就是”f“;parent是Function.prototype,这是一个对象,它没有name属性,所以第二句应该是啥都不输出;eval()函数会将传入的字符串当做 JavaScript 代码进行执行,eval(f.name)相当于eval('f'),执行结果是输出函数f的内容,type of计算返回function;最后一句返回空; 34. exp.testvar lowerCaseOnly = /^[a-z]+$/;console.log([lowerCaseOnly.test(null), lowerCaseOnly.test()]);上面语句输出什么内容? RegExp.prototype.test()方法的参数是一个字符串,如果不是字符串会尝试转换成字符串。所以上面两句相当于lowerCaseOnly.test("null"),lowerCaseOnly.test(”Undefined“),所以返回[true, true] 数组元素最后一个逗号console.log([,,,].join(", ")); 上面的表达式输出什么内容? 我们定义一个有三个元素的数组的时候可以这样var arr = [1, 2, 3]; 也可以这样var arr = [1, 2, 3,];它后面了一个逗号,但是还是三个元素。本题的关键点就是[, , ,],这其实是一个有三个空元素的数组,输出 [empty × 3]。三个空元素用逗号连接起来最后输出是两个逗号,因为都是空元素,其实最后一个逗号后面是有一个空元素的。所以本题输出",,"。如下图: 其实定义一个对象也可以在最后一个属性后面加上一个逗号,例如 var obj = { name: 'zhangsan', age: 20, }; class关键字 a = {class: "Animal", name: 'Fido'}; console.log(a.class);上面的语句输出什么? 这一题是考察class关键字,我在chrome里输出的是正确的值“Animal”,记住在定义变量或者属性名字的时候尽量避免JavaScript关键字。如果属性里有关键字,可以这样使用a["class"]。 时间转换问题 a = new Date("epoch") console.log(a);上面的表达式输出什么? new Date()构造函数传入的必须是一个时间字符串,即可以通过Date.parse()解析成功。所以本题输出Invalid Date。 函数的形参个数 a = Function.length,b = new Function().lengthconsole.log(a === b); 上面表达式输出什么? Function 构造器本身也是个Function。他的 ength属性值为 1,所以a=1。该属性 Writable: false, Enumerable: false, Configurable: true。但是new Function()是一个对象,他没有形参,说以b=0,本题输出false。另外函数的实参的个数可以用argument.length获取。 还是时间转换问题 a = Date(0); var b = new Date(0);var c = new Date();console.log([a === b, b === c, a === c]);上面的语句输出什么? 直接调用函数Date(),得到的结果是一个表示时间的字符串;使用new操作符+构造函数,得到的是一个时间对象;参数是0返回的是格林威治0时,不带参数返回的是当前时间;搞清楚这些之后这题就简单了,a是一个时间字符串,b是一个时间对象,表示格林威治0时,c也是一个时间对象,不过是当前时间。所以本题输出[false, false, false] Math.max()&Math.min() min = Math.min(), max = Math.max() console.log(min < max);上面语句输出什么? 注意Max.max()和Max.min()传入的参数是一个数组,如果不传参,前者返回+Infinity,后者返回-Infinity。Infinity不能做比较,所以本题返回false。 正则表达式的“记忆” function captureOne(re, str) { var match = re.exec(str); return match && match[1]; }var numRe = /num=(d+)/ig, wordRe = /word=(\w+)/i, a1 = captureOne(numRe, "num=1"), a2 = captureOne(wordRe, "word=1"), a3 = captureOne(numRe, "NUM=2"), a4 = captureOne(wordRe, "WORD=2"); console.log([a1 === a2, a3 === a4]); 上面表达式输出什么?这题考察的是正则表达式的/g选项,这个选项表示全局匹配;找到所有匹配,而不是在第一个匹配后停止。来看下面的例子: var myRe = /ab*/g;var str = 'abbcdefabh';console.log(myRe.exec(str));console.log(myRe.exec(str));输出结果如下: 从结果可以看出,执行两次都有返回结果,分别找到字符串中两处符合条件的匹配。 而原题中第一个正在有/g选项,第一次匹配成功之后会从当前位置往后查找,所以在执行a3 = captureOne(numRe, "NUM=2")的时候不是从字符串位置0开始查找的,而是从第5位开始,所有匹配就失败了,找不出1。 所以本题输出[true, false]。 Date中的month a = new Date("2014-03-19"),b = new Date(2014, 3, 19);console.log([a.getDate() === b.getDate(), a.getMonth() === b.getMonth()]); 上面表达式输出什么?这个问题考察的是Date对象中的月份问题,注意使用第二种方式定义时间对象, new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);第二个参数是monthIndex,它是从0开始的,3表示4月份,所以本题输出[false, false]。如果初学JavaScript,感觉套路深啊套路深。 正则表达式中的转义 ('http://giftwrapped.com/picture.jpg'.match('.gif')) { console.log('a gif file');} else { console.log('not a gif file'); }String.prototype.match 接受一个正则, 如果不是, 按照 new RegExp(obj) 转化. 所以 . 并不会转义,所以开头的‘http://gifwrapped......’中/gif就匹配了 /.gif/,所以输出“a gif file” 变量提升 function foo(a) { var a; return a; }function bar(a) { var a = 'bye'; return a; }console.log([foo('hello'), bar('hello')]); 上面语句输出什么?a作为参数其实已经声明了, 所以 var a; var a = 'bye' 其实就是 a; a ='bye',所以本题输出["hello", "bye"]。 JavaScript语法本来就有很多不合理的地方,导致书写JavaScript很容易出错,本文中如有错误,欢迎各位看官提出来。 作者:Tyler Ning出处:http://www.cnblogs.com/tylerdonet/
mybatis源码配置文件解析之三:解析typeAliases标签 在前边的博客在分析了mybatis解析settings标签,《mybatis源码配置文件解析之二:解析settings标签》。下面来看解析typeAliases标签的过程。 一、概述在mybatis核心配置文件(mybatis-config.xml)中有关typeAliases的配置如下, <package name="cn.com.mybatis.bean"></package> <typeAlias name="user" type="cn.com.mybatis.bean.User"></typeAlias> 上面给出了两种配置typeAlias的放式,一种是配置package标签,一种是typeAlias表。 我上面的配置是有问题的,在测试的时候一直报下面的错误, 上面的问题困扰了笔者好久,没找到原因,因为解析typeAliases标签的源码中找不到任何的原因,最后排查日志,原来是在加载核心配置文件的时候要把配置和mybatis的dtd文件进行验证,这里是验证出错了,具体的错误是typeAlias标签必须在package标签的前边,也就是标签是有顺序的。把配置改为下面的顺序,程序正常, <typeAlias alias="user" type="cn.com.mybatis.bean.User"></typeAlias> <package name="cn.com.mybatis.bean"/> </typeAliases> 1、配置标签标签配置的是一个包名,mybatis会扫描该包下的所有类,并注册一个别名,这里在标签中无法为某个类指定一个自定义的别名,mybatis提供了另外一种方式可以使用自定义的别名,即@Alias注解,在类上标记该注解,如下, package cn.com.mybatis.bean; import org.apache.ibatis.type.Alias; //配置别名为myMenu@Alias(value="myMenu")public class Menu { private String menuId; private String menuName; private String url; } 上面为Menu类配置了别名,在扫描该包的时候会使用自定义的别名,不会使用mybatis默认的别名规则(Class.getSimpleName()) 2、配置标签这种配置是单独为某个类配置别名,其中alias属性可以不配置,不配置则使用mybatis默认的别名规则,如下 上面看了typeAlias的两种配置方式,那么何为typeAlias,意思就是给一个类配置一个别名,如这里有一个cn.com.mybatis.bean.User类,可以为其配置别名为MyUser, 那么在配置文件中便可以使用别名代替类的全限类名,目的是简便。这里需要注意的是配置的别名的使用范围仅限于mybatis的配置文件中(包含核心配置文件和Mpper映射文件) 二、详述上面,了解了typeAlias的配置及作用,下面看mybatis是如何解析的。 在XMLConfigBuilder类中的parseConfiguration方法, private void parseConfiguration(XNode root) { try { //issue #117 read properties first //解析properties标签 propertiesElement(root.evalNode("properties")); //解析settings标签,1、把<setting>标签解析为Properties对象 Properties settings = settingsAsProperties(root.evalNode("settings")); /*2、对<settings>标签中的<setting>标签中的内容进行解析,这里解析的是<setting name="vfsImpl" value=","> * VFS是mybatis中用来表示虚拟文件系统的一个抽象类,用来查找指定路径下的资源。上面的key为vfsImpl的value可以是VFS的具体实现,必须 * 是权限类名,多个使用逗号隔开,如果存在则设置到configuration中的vfsImpl属性中,如果存在多个,则设置到configuration中的仅是最后一个 * */ loadCustomVfs(settings); //解析别名标签,例<typeAlias alias="user" type="cn.com.bean.User"/> typeAliasesElement(root.evalNode("typeAliases")); //解析插件标签 pluginElement(root.evalNode("plugins")); //解析objectFactory标签,此标签的作用是mybatis每次创建结果对象的新实例时都会使用ObjectFactory,如果不设置 //则默认使用DefaultObjectFactory来创建,设置之后使用设置的 objectFactoryElement(root.evalNode("objectFactory")); //解析objectWrapperFactory标签 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //解析reflectorFactory标签 reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 //解析environments标签 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); //解析<mappers>标签 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 从上面可以看出typeAliasesElement方法,此方法用来解析typeAliases标签及其子标签, private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { //1、解析package标签 if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { //2、解析typeAlias标签 String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); try { Class<?> clazz = Resources.classForName(type); if (alias == null) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } } typeAliasesElement方法会分别解析typeAliases标签的package和typeAlias子标签。通过上面的分析知道在配置的时候标签要在标签前边,但这里按照源码的顺序先分析标签的解析。 1、解析标签下面看typeAliasesElement方法中对package标签的解析, if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } 从上面可以看到获取标签的name属性,也就配置的包名,然后调用下面的方法, configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);可以看到从Configuration中获得TypeAliasRegistry,然后调用其registerAliases方法, public void registerAliases(String packageName){ registerAliases(packageName, Object.class); }又调用另外一个方法,如下, /** 为包下的所有java bean注册别名 @param packageName @param superType*/ public void registerAliases(String packageName, Class<?> superType){ ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); //把该包下的所有类进行加载,把其Class对象放到resolverUtil的matches中 resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); for(Class<?> type : typeSet){ // Ignore inner classes and interfaces (including package-info.java) // Skip also inner classes. See issue #6 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { registerAlias(type); } } } 上面方法的作用是遍历给的的包名,把该包下的所有的类进行加载,并放到resolverUtil中的matches中,这里具体的遍历方法暂且不看。遍历完成后取出resolverUtil中的所有Class对象,只要不是匿名类、接口则执行registerAlias方法, public void registerAlias(Class<?> type) { //获得类的简单类名,如cn.com.mybatis.bean.User 则其简单名称为User String alias = type.getSimpleName(); //判断类上是否存在@Alias注解 Alias aliasAnnotation = type.getAnnotation(Alias.class); //如果存在@Alias注解,则使用注解上配置的value属性作为别名 if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } registerAlias(alias, type); } 看上面的方法,上面的方法先获得Class的简单类名, //获得类的简单类名,如cn.com.mybatis.bean.User 则其简单名称为User String alias = type.getSimpleName(); 然后会判断类上是否有@Alias注解,如果有则取其value值作为类的别名, //判断类上是否存在@Alias注解 Alias aliasAnnotation = type.getAnnotation(Alias.class); //如果存在@Alias注解,则使用注解上配置的value属性作为别名 if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } 进行上面的判断,存在@Alias注解,使用其value值作为别名,否则使用类的简单类名(Class.getSimpleName()),然后执行registerAlias方法, public void registerAlias(String alias, Class<?> value) { if (alias == null) { throw new TypeException("The parameter alias cannot be null"); } // issue #748 String key = alias.toLowerCase(Locale.ENGLISH); //如果已经注册了改别名则会抛异常 if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); } TYPE_ALIASES.put(key, value); } 上面的代码会把别名转化为英文的小写作为存入的key,使用对应的Class存入TYPE_ALIASES中。如果已经注册过该key则会抛出异常,也就是不允许重复注册或者相同的key是无法覆盖的。这里还有一个问题,如果我们配置的是别名中含有大写,那么注册的时候是小写的,在使用的时候是用配置的还是用注册的,例,上面的例子, package cn.com.mybatis.bean; import org.apache.ibatis.type.Alias; //配置别名为myMenu@Alias(value="myMenu")public class Menu { private String menuId; private String menuName; private String url; } 这里配置的是myMenu,注册的确实下面的 可以看到注册之后的是mymenu。其实在使用的时候是大小写不敏感的,在匹配的时候会统一转化为小写,这样就可以对应TYPE_ALIASES中已注册的别名。 2、解析标签上面分析了标签的解析过程,下面看有关标签的解析, 解析标签即是获取alias和type两个属性,可以看到对alias进行了判断,也就说可以不配置alias属性,那么会使用下面的方法处理 public void registerAlias(Class<?> type) { //获得类的简单类名,如cn.com.mybatis.bean.User 则其简单名称为User String alias = type.getSimpleName(); //判断类上是否存在@Alias注解 Alias aliasAnnotation = type.getAnnotation(Alias.class); //如果存在@Alias注解,则使用注解上配置的value属性作为别名 if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } registerAlias(alias, type); } 该方法前面已分析,会判断配置的类是否含有@Alias注解,如果有则使用注解上的value值。这里存在一个问题,如果在标签中配置了alias,在类上也有@Alias注解,且不一样,以哪个为准,通过上面的分析,得出下面的结论, 在使用标签的时候,配置了alias属性,在类上也有@Alias(value="myAlias2"),已配置的为准(最终别名为myAlias) 下面看registerAlias(alias,type)方法, public void registerAlias(String alias, Class<?> value) { if (alias == null) { throw new TypeException("The parameter alias cannot be null"); } // issue #748 String key = alias.toLowerCase(Locale.ENGLISH); //如果已经注册了改别名则会抛异常 if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); } TYPE_ALIASES.put(key, value); } 此方法上面分析过,如果存在相同的key会抛异常,最终存入TYPE_ALIASES中。 三、总结本文分析了mybatis核心配置文件(mybatis-config.xml)的标签的配置及源码解析。 另在写Mapper映射文件和核心配置文件的时候会使用一些自定义的别名,这些别名是怎么注册的那,在Configuration、TypeAliasRegistry类中进行了注册,如下Configuration, public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); languageRegistry.register(RawLanguageDriver.class); } 在TypeAliasRegistry中注册了下面的别名, //默认的构造方法,初始化系统内置的别名 public TypeAliasRegistry() { registerAlias("string", String.class); registerAlias("byte", Byte.class); registerAlias("long", Long.class); registerAlias("short", Short.class); registerAlias("int", Integer.class); registerAlias("integer", Integer.class); registerAlias("double", Double.class); registerAlias("float", Float.class); registerAlias("boolean", Boolean.class); registerAlias("byte[]", Byte[].class); registerAlias("long[]", Long[].class); registerAlias("short[]", Short[].class); registerAlias("int[]", Integer[].class); registerAlias("integer[]", Integer[].class); registerAlias("double[]", Double[].class); registerAlias("float[]", Float[].class); registerAlias("boolean[]", Boolean[].class); registerAlias("_byte", byte.class); registerAlias("_long", long.class); registerAlias("_short", short.class); registerAlias("_int", int.class); registerAlias("_integer", int.class); registerAlias("_double", double.class); registerAlias("_float", float.class); registerAlias("_boolean", boolean.class); registerAlias("_byte[]", byte[].class); registerAlias("_long[]", long[].class); registerAlias("_short[]", short[].class); registerAlias("_int[]", int[].class); registerAlias("_integer[]", int[].class); registerAlias("_double[]", double[].class); registerAlias("_float[]", float[].class); registerAlias("_boolean[]", boolean[].class); registerAlias("date", Date.class); registerAlias("decimal", BigDecimal.class); registerAlias("bigdecimal", BigDecimal.class); registerAlias("biginteger", BigInteger.class); registerAlias("object", Object.class); registerAlias("date[]", Date[].class); registerAlias("decimal[]", BigDecimal[].class); registerAlias("bigdecimal[]", BigDecimal[].class); registerAlias("biginteger[]", BigInteger[].class); registerAlias("object[]", Object[].class); registerAlias("map", Map.class); registerAlias("hashmap", HashMap.class); registerAlias("list", List.class); registerAlias("arraylist", ArrayList.class); registerAlias("collection", Collection.class); registerAlias("iterator", Iterator.class); registerAlias("ResultSet", ResultSet.class); } 上面两个类注册了系统内置的别名,在核心配置文件和Mapper映射文件中可使用,mybatis会自动映射其注册类型,且大小写不区分。 原文地址https://www.cnblogs.com/teach/p/12766760.html
写给程序员的机器学习入门 (三) - 线性模型,激活函数与多层线性模型生物神经元与人工神经元在了解神经元网络之前,我们先简单的看看生物学上的神经元是什么样子的,下图摘自维基百科: (因为我不是专家,这里的解释只用于理解人工神经元模拟了生物神经元的什么地方,不一定完全准确) 神经元主要由细胞体和细胞突组成,而细胞突分为树突 (Dendrites) 和轴突 (Axon),树突负责接收其他神经元输入的电流,而轴突负责把电流输出给其他神经元。一个神经元可以通过树突从多个神经元接收电流,如果电流没有达到某个阈值则神经元不会把电流输出,如果电流达到了某个阈值则神经元会通过轴突的突触把电流输出给其他神经元,这样的规则被称为全有全无律。输入电流达到阈值以后输出电流的状态又称为到达动作电位,动作电位会持续 1 ~ 2 毫秒,之后会进入约 0.5 毫秒的绝对不应期,无论输入多大的电流都不会输出,然后再进入约 3.5 毫秒的相对不应期,需要电流达到更大的阈值才会输出,最后返回静息电位。神经元之间连接起来的网络称为神经元网络,人的大脑中大约有 860 亿个神经元,因为 860 亿个神经元可以同时工作,所以目前的计算机无法模拟这种工作方式 (除非开发专用的芯片),只能模拟一部分的工作方式和使用更小规模的网络。 计算机模拟神经元网络使用的是人工神经元,单个人工神经元可以用以下公式表达: 其中 n 代表输入的个数,你可以把 n 看作这个神经元拥有的树突个数,x 看作每个树突输入电流的值;而 w (weight) 代表各个输入的权重,也就是各个树突对电流大小的调整;而 b (bias) 用于调整各个输入乘权重相加后的值,使得这个值可以配合某个阈值工作;而 g 则是激活函数,用于判断值是否达到阈值并输出和输出多少,通常会使用非线性函数;而 y 则是输出的值,可以把它看作轴突输出的电流,连接这个 y 到其他神经元就可以组建神经元网络。 我们在前两篇看到的其实就是只有一个输入并且没有激活函数的单个人工神经元,把同样的输入传给多个神经元 (第一层),然后再传给其他神经元 (第二层),然后再传给其他神经元 (第三层) 就可以组建人工神经元网络了,同一层的神经元个数越多,神经元的层数越多,网络就越强大,但需要更多的运算时间并且更有可能发生第一篇文章讲过的过拟合 (Overfitting) 现象。 下图是人工神经元网络的例子,有 3 输入 1 个输出,经过 3 层处理,第 1 层和第 2 层各有两个神经元对应隐藏值 (中间值),第 3 层有一个神经元对应输出值: 神经元中包含的 w 和 b 就是我们需要通过机器学习调整的参数值。 如果你觉得图片有点难以理解,可以看转换后的代码: h11 = g(x1 w111 + x2 w112 + x3 * w113 + b11)h12 = g(x1 w121 + x2 w122 + x3 * w123 + b12)h21 = g(h11 w211 + h12 w212 + b21)h22 = g(h11 w221 + h12 w222 + b22)y = g(h21 w311 + h22 w312 + b31)很多痴迷人工神经元网络的学者声称人工神经元网络可以模拟人脑的工作方式,做到某些领域上超过人脑的判断,但实际上这还有很大的争议,我们可以看到人工神经元的连接方式只会按固定的模式,判断是否达到阈值并输出的逻辑也无法做到和生物神经元一样(目前还没有解明),并且也没有生物神经元的不应期,所以也有学者声称人工神经元不过只是做了复杂的数学运算来模拟逻辑判断,需要根据不同的场景切换不同的计算方法,使用这种方式并不能达到人脑的水平。 单层线性模型在前一篇文章我们已经稍微了解过机器学习框架 pytorch,现在我们来看看怎么使用 pytorch 封装的线性模型,以下代码运行在 python 的 REPL 中: 导入 pytorch 类库 import torch 创建 pytorch 封装的线性模型,设置输入有 3 个输出有 1 个 model = torch.nn.Linear(in_features=3, out_features=1) 查看线性模型内部包含的参数列表 这里一共包含两个参数,第一个参数是 1 行 3 列的矩阵分别表示 3 个输入对应的 w 值 (权重),第二个参数表示 b 值 (偏移) 初始值会随机生成 (使用 kaiming_uniform 生成正态分布) list(model.parameters()) [Parameter containing:tensor([[0.0599, 0.1324, 0.0099]], requires_grad=True), Parameter containing:tensor([-0.2772], requires_grad=True)] 定义输入和输出 x = torch.tensor([1, 2, 3], dtype=torch.float)y = torch.tensor([6], dtype=torch.float) 把输入传给模型 p = model(x) 查看预测输出值 1 0.0599 + 2 0.1324 + 3 * 0.0099 - 0.2772 = 0.0772 p tensor([0.0772], grad_fn=) 计算误差并自动微分 l = (p - y).abs()l tensor([5.9228], grad_fn=) l.backward() 查看各个参数对应的导函数值 list(model.parameters())[0].grad tensor([[-1., -2., -3.]]) list(model.parameters())[1].grad tensor([-1.])以上可以看作 1 层 1 个神经元,很好理解吧?我们来看看 1 层 2 个神经元: 导入 pytorch 类库 import torch 创建 pytorch 封装的线性模型,设置输入有 3 个输出有 2 个 model = torch.nn.Linear(in_features=3, out_features=2) 查看线性模型内部包含的参数列表 这里一共包含两个参数 第一个参数是 2 行 3 列的矩阵分别表示 2 个输出和 3 个输入对应的 w 值 (权重) 第二个参数表示 2 个输出对应的 b 值 (偏移) list(model.parameters()) [Parameter containing:tensor([[0.1393, 0.5165, 0.2910], [0.2276, 0.1579, 0.1958]], requires_grad=True), Parameter containing: tensor([0.2566, 0.1701], requires_grad=True)] 定义输入和输出 x = torch.tensor([1, 2, 3], dtype=torch.float)y = torch.tensor([6, -6], dtype=torch.float) 把输入传给模型 p = model(x) 查看预测输出值 1 0.1393 + 2 0.5165 + 3 * 0.2910 + 0.2566 = 2.3019 1 0.2276 + 2 0.1579 + 3 * 0.1958 + 0.1701 = 1.3009 p tensor([2.3019, 1.3009], grad_fn=) 计算误差并自动微分 (abs(2.3019 - 6) + abs(1.3009 - -6)) / 2 = 5.4995 l = (p - y).abs().mean()l tensor(5.4995, grad_fn=) l.backward() 查看各个参数对应的导函数值 因为误差取了 2 个值的平均,所以求导函数值的时候会除以 2 list(model.parameters())[0].grad tensor([[-0.5000, -1.0000, -1.5000], [ 0.5000, 1.0000, 1.5000]]) list(model.parameters())[1].gradtensor([-0.5000, 0.5000]) 现在我们来试试用线性模型来学习符合 x_1 1 + x_2 2 + x_3 * 3 + 8 = y 的数据,输入和输出会使用矩阵定义: 引用 pytorch import torch 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数 这是为了让训练过程可重现,你也可以选择不这样做 torch.random.manual_seed(0) 创建线性模型,设置有 3 个输入 1 个输出 model = torch.nn.Linear(in_features=3, out_features=1) 创建损失计算器 loss_function = torch.nn.MSELoss() 创建参数调整器 optimizer = torch.optim.SGD(model.parameters(), lr=0.01) 随机生成原始数据集,一共 20 组数据,每条数据有 3 个输入 dataset_x = torch.randn((20, 3))dataset_y = dataset_x.mm(torch.tensor([[1], [2], [3]], dtype=torch.float)) + 8print(f"dataset_x: {dataset_x}")print(f"dataset_y: {dataset_y}") 切分训练集 (12 组),验证集 (4 组) 和测试集 (4 组) random_indices = torch.randperm(dataset_x.shape[0])traning_indices = random_indices[:int(len(random_indices)*0.6)]validating_indices = random_indices[int(len(random_indices)0.6):int(len(random_indices)0.8):]testing_indices = random_indices[int(len(random_indices)*0.8):]traning_set_x = dataset_x[traning_indices]traning_set_y = dataset_y[traning_indices]validating_set_x = dataset_x[validating_indices]validating_set_y = dataset_y[validating_indices]testing_set_x = dataset_x[testing_indices]testing_set_y = dataset_y[testing_indices] 开始训练过程 for epoch in range(1, 10000): print(f"epoch: {epoch}") # 根据训练集训练并修改参数 # 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropout model.train() # 计算预测值 # 20 行 3 列的矩阵乘以 3 行 1 列的矩阵 (由 weight 转置得到) 等于 20 行 1 列的矩阵 predicted = model(traning_set_x) # 计算损失 loss = loss_function(predicted, traning_set_y) # 打印除错信息 print(f"loss: {loss}, weight: {model.weight}, bias: {model.bias}") # 从损失自动微分求导函数值 loss.backward() # 使用参数调整器调整参数 optimizer.step() # 清空导函数值 optimizer.zero_grad() # 检查验证集 # 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropout model.eval() predicted = model(validating_set_x) validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).abs().mean() print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}") # 如果验证集正确率大于 99 %,则停止训练 print(f"validating accuracy: {validating_accuracy}") if validating_accuracy > 0.99: break 检查测试集 predicted = model(testing_set_x)testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).abs().mean()print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")print(f"testing accuracy: {testing_accuracy}")输出结果如下: dataset_x: tensor([[ 0.8487, 0.6920, -0.3160], [-2.1152, -0.3561, 0.4372], [ 0.4913, -0.2041, 0.1198], [ 1.2377, 1.1168, -0.2473], [-1.0438, -1.3453, 0.7854], [ 0.9928, 0.5988, -1.5551], [-0.3414, 1.8530, 0.4681], [-0.1577, 1.4437, 0.2660], [ 1.3894, 1.5863, 0.9463], [-0.8437, 0.9318, 1.2590], [ 2.0050, 0.0537, 0.4397], [ 0.1124, 0.6408, 0.4412], [-0.2159, -0.7425, 0.5627], [ 0.2596, 0.5229, 2.3022], [-1.4689, -1.5867, -0.5692], [ 0.9200, 1.1108, 1.2899], [-1.4782, 2.5672, -0.4731], [ 0.3356, -1.6293, -0.5497], [-0.4798, -0.4997, -1.0670], [ 1.1149, -0.1407, 0.8058]]) dataset_y: tensor([[ 9.2847], [ 6.4842], [ 8.4426], [10.7294], [ 6.6217], [ 5.5252], [12.7689], [11.5278], [15.4009], [12.7970], [11.4315], [10.7175], [ 7.9872], [16.2120], [ 1.6500], [15.0112], [10.2369], [ 3.4277], [ 3.3199], [11.2509]]) epoch: 1loss: 142.77590942382812, weight: Parameter containing:tensor([[-0.0043, 0.3097, -0.4752]], requires_grad=True), bias: Parameter containing:tensor([-0.4249], requires_grad=True)validating x: tensor([[-0.4798, -0.4997, -1.0670], [ 0.8487, 0.6920, -0.3160], [ 0.1124, 0.6408, 0.4412], [-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199], [ 9.2847], [10.7175], [ 6.6217]]), predicted: tensor([[-0.1385], [ 0.3020], [-0.0126], [-1.1801]], grad_fn=<AddmmBackward>) validating accuracy: -0.04714548587799072epoch: 2loss: 131.40403747558594, weight: Parameter containing:tensor([[ 0.0675, 0.4937, -0.3163]], requires_grad=True), bias: Parameter containing:tensor([-0.1970], requires_grad=True)validating x: tensor([[-0.4798, -0.4997, -1.0670], [ 0.8487, 0.6920, -0.3160], [ 0.1124, 0.6408, 0.4412], [-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199], [ 9.2847], [10.7175], [ 6.6217]]), predicted: tensor([[-0.2023], [ 0.6518], [ 0.3935], [-1.1479]], grad_fn=<AddmmBackward>) validating accuracy: -0.03184401988983154epoch: 3loss: 120.98343658447266, weight: Parameter containing:tensor([[ 0.1357, 0.6687, -0.1639]], requires_grad=True), bias: Parameter containing:tensor([0.0221], requires_grad=True)validating x: tensor([[-0.4798, -0.4997, -1.0670], [ 0.8487, 0.6920, -0.3160], [ 0.1124, 0.6408, 0.4412], [-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199], [ 9.2847], [10.7175], [ 6.6217]]), predicted: tensor([[-0.2622], [ 0.9860], [ 0.7824], [-1.1138]], grad_fn=<AddmmBackward>) validating accuracy: -0.016991496086120605 省略途中输出 epoch: 637loss: 0.001102567883208394, weight: Parameter containing:tensor([[1.0044, 2.0283, 3.0183]], requires_grad=True), bias: Parameter containing:tensor([7.9550], requires_grad=True)validating x: tensor([[-0.4798, -0.4997, -1.0670], [ 0.8487, 0.6920, -0.3160], [ 0.1124, 0.6408, 0.4412], [-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199], [ 9.2847], [10.7175], [ 6.6217]]), predicted: tensor([[ 3.2395], [ 9.2574], [10.6993], [ 6.5488]], grad_fn=<AddmmBackward>) validating accuracy: 0.9900396466255188testing x: tensor([[-0.3414, 1.8530, 0.4681], [-1.4689, -1.5867, -0.5692], [ 1.1149, -0.1407, 0.8058], [ 0.3356, -1.6293, -0.5497]]), y: tensor([[12.7689], [ 1.6500], [11.2509], [ 3.4277]]), predicted: tensor([[12.7834], [ 1.5438], [11.2217], [ 3.3285]], grad_fn=<AddmmBackward>) testing accuracy: 0.9757462739944458可以看到最终 weight 接近 1, 2, 3,bias 接近 8。和前一篇文章最后的例子比较还可以发现代码除了定义模型的部分以外几乎一模一样 (后面的代码基本上都是相同的结构,这个系列是先学套路在学细节) 。 看到这里你可能会觉得,怎么我们一直都在学习一次方程式,不能做更复杂的事情吗? 如我们看到的,线性模型只能计算一次方程式,如果我们给的数据不满足任何一次方程式,这个模型将无法学习成功,那么叠加多层线性模型可以学习更复杂的数据吗? 以下是一层和两层人工神经元网络的公式例子: 因为我们还没有学到激活函数,先去掉激活函数,然后展开没有激活函数的两层人工神经元网络看看是什么样子: 从上图可以看出,如果没有激活函数,两层人工神经元网络和一层神经元网络效果是一样的,实际上,如果没有激活函数不管叠加多少层都和一层一样,所以如果我们要构建多层网络,必须添加激活函数。 激活函数激活函数的作用是让人工神经元网络支持学习非线性的数据,所谓非线性的数据就是不满足任何一次方程式的数据,输出和输入之间不会按一定比例变化。举个很简单的例子,如果需要按码农的数量计算某个项目所需的完工时间,一个码农需要一个月,两个码农需要半个月,三个码农需要十天,四个码农需要一个星期,之后无论请多少个码农都需要一个星期,多出来的码农只会吃闲饭,使用图表可以表现如下: 如果不用激活函数,只用线性模型可以调整 w 到 -5.4,b 到 30,使用图表对比 -5.4 x + 30 和实际数据如下,我们可以看到不仅预测的误差较大,随着码农数量的增长预测所需的完工时间会变为负数: 那么使用激活函数会怎样呢?我们以最简单也是最流行的激活函数 ReLU (Rectified Linear Unit) 为例,ReLU 函数的定义如下: 意思是传入值大于 0 时返回原值,否则返回 0,用 python 代码可以表现如下: def relu(x): if x > 0: return x return 0 再看看以下结合了 ReLU 激活函数的两层人工神经元网络 (最后一层不使用激活函数): 试试计算上面的例子: def relu(x): if x > 0: return x return 0 for x in range(1, 11): h1 = relu(x * -1 + 2) h2 = relu(x * -1 + 3) h3 = relu(x * -1 + 4) y = h1 * 10 + h2 * 2 + h3 * 3 + 7 print(x, y) 输入如下,可以看到添加激活函数后模型能够支持计算上面的非线性数据: 1 302 153 104 75 76 77 78 79 710 7激活函数除了上述介绍的 ReLU 还有很多,以下是一些常用的激活函数: 如果你想看它们的曲线和导函数可以参考以下链接: https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearityhttps://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html添加激活函数以后求导函数值同样可以使用连锁律,如果第一层返回的是正数,那么就和没有 ReLU 时的计算一样 (导函数为 1): w1 = torch.tensor(5.0, requires_grad=True)b1 = torch.tensor(1.0, requires_grad=True)w2 = torch.tensor(6.0, requires_grad=True)b2 = torch.tensor(7.0, requires_grad=True) x = torch.tensor(2.0)y = torch.nn.functional.relu(x w1 + b1) w2 + b2 假设我们要调整 w1, b1, w2, b2 使得 y 接近 0 y tensor(73., grad_fn=) y.abs().backward() w1 的导函数值为 x * w2 w1.grad tensor(12.) b1 的导函数值为 w2 b1.grad tensor(6.) w2 的导函数值为 x * w1 + b1 w2.grad tensor(11.) b1 的导函数值为 1 b2.grad tensor(1.)假设第一层返回的是负数,那么 ReLU 会让上一层的导函数值为 0 (导函数为 0): w1 = torch.tensor(5.0, requires_grad=True)b1 = torch.tensor(1.0, requires_grad=True)w2 = torch.tensor(6.0, requires_grad=True)b2 = torch.tensor(7.0, requires_grad=True) x = torch.tensor(2.0)y = torch.nn.functional.relu(x w1 + b1) w2 + b2 y tensor(7., grad_fn=) y.backward() w1.grad tensor(-0.) b1.grad tensor(0.) w2.grad tensor(0.) b2.grad tensor(1.)虽然 ReLU 可以适合大部分场景,但有时候我们还是需要选择其他激活函数,选择激活函数一般会考虑以下的因素: 计算量是否存在梯度消失 (Vanishing Gradient) 问题是否存在停止学习问题从上图我们可以看到 ReLU 的计算量是最少的,Sigmoid 和 Tanh 的计算量则很大,使用计算量大的激活函数会导致学习过程更慢。 而梯度消失 (Vanishing Gradient, 之前的文章把 Gradient 翻译为倾斜,但数学上的正确叫法是梯度) 问题则是在人工神经元网络层数增多以后出现的问题,如果层数不断增多,使用连锁律求导函数的时候会不断叠加激活函数的导函数,部分激活函数例如 Sigmoid 和 Tanh 的导函数会随着叠加次数增多而不断的减少导函数值。例如有 3 层的时候,第 3 层导函数值可能是 6, -2, 1,第 2 层的导函数值可能是 0.07, 0.68, -0.002,第 1 层的导函数值可能是 0.0004, -0.00016, -0.00003,也就是前面的层参数基本上不会调整,只有后面的层参数不断变化,导致浪费计算资源和不能完全发挥模型的能力。激活函数 ReLU 则不会存在梯度消失问题,因为不管叠加多少层只要中间不存在负数则导函数值会一直传递上去,这也是 ReLU 流行的原因之一。 停止学习问题是模型达到某个状态 (未学习成功) 以后不管怎么调整参数都不会变化的问题,一个简单的例子是使用 ReLU 时,如果第一层的输出刚好全部都是负数,那隐藏值则全部为 0,导函数值也为 0,不管再怎么训练参数都不会变化。LeakyReLU 与 ELU 则是为了解决停止学习问题产生的,但因为增加计算量和允许负数可能会带来其他影响,我们一般都会先使用 ReLU,出现停止学习问题再试试 ReLU 的派生函数。 Sigmoid 和 Tanh 虽然有梯度消失问题,但是它们可以用于在指定场景下转换数值到 0 ~ 1 和 -1 ~ 1。例如 Sigmoid 可以用在最后一层表现可能性,100 表示非常有可能 (转换到 1),50 也代表非常有可能 (转换到 1),1 代表比较有可能 (转换到 0.7311),0 代表不确定 (转换到 0.5),-1 代表比较不可能 (转换到 0.2689),-100 代表很不可能 (转换到 0)。而后面文章介绍的 LSTM 模型也会使用 Sigmoid 决定需要忘记哪些内部状态,Tanh 决定应该怎样更新内部状态。此外还有 Softmax 等一般只用在最后一层的函数,Softmax 可以用于在分类的时候判断哪个类别可能性最大,例如识别猫狗猪的时候最后一层给出 6, 5, 8,数值越大代表属于该分类的可能性越高,经过 Softmax 转换以后就是 0.1142, 0.0420, 0.8438,代表有 11.42% 的可能性是猫,4.2% 的可能性是狗,84.38% 的可能性是猪。 多层线性模型接下来我们看看怎样在 pytorch 里面定义多层线性模型,上一节已经介绍过 torch.nn.Linear 是 pytorch 中单层线性模型的封装,组合多个 torch.nn.Linear 就可以实现多层线性模型。 组合 torch.nn.Linear 有两种方法,一种创建一个自定义的模型类,关于模型类在上一篇已经介绍过: 引用 pytorch,nn 等同于 torch.nn import torchfrom torch import nn 定义模型 class MyModel(nn.Module): def __init__(self): # 初始化基类 super().__init__() # 定义参数 # 这里一共定义了三层 # 第一层接收 2 个输入,返回 32 个隐藏值 (内部 weight 矩阵为 32 行 2 列) # 第二层接收 32 个隐藏值,返回 64 个隐藏值 (内部 weight 矩阵为 64 行 32 列) # 第三层接收 64 个隐藏值,返回 1 个输出 (内部 weight 矩阵为 1 行 64 列) self.layer1 = nn.Linear(in_features=2, out_features=32) self.layer2 = nn.Linear(in_features=32, out_features=64) self.layer3 = nn.Linear(in_features=64, out_features=1) def forward(self, x): # x 是一个矩阵,行数代表批次,列数代表输入个数,例如有 50 个批次则为 50 行 2 列 # 计算第一层返回的隐藏值,例如有 50 个批次则 hidden1 为 50 行 32 列 # 计算矩阵乘法时会转置 layer1 内部的 weight 矩阵,50 行 2 列乘以 2 行 32 列等于 50 行 32 列 hidden1 = nn.functional.relu(self.layer1(x)) # 计算第二层返回的隐藏值,例如有 50 个批次则 hidden2 为 50 行 64 列 hidden2 = nn.functional.relu(self.layer2(hidden1)) # 计算第三层返回的输出,例如有 50 个批次则 y 为 50 行 1 列 y = self.layer3(hidden2) # 返回输出 return y 创建模型实例 model = MyModel()我们可以定义任意数量的层,但每一层的接收值个数 (in_features) 必须等于上一层的返回值个数 (out_features),第一层的接收值个数需要等于输入个数,最后一层的返回值个数需要等于输出个数。 第二种方法是使用 torch.nn.Sequential,这种方法更简便: 引用 pytorch,nn 等同于 torch.nn import torchfrom torch import nn 创建模型实例,效果等同于第一种方法 model = nn.Sequential( nn.Linear(in_features=2, out_features=32), nn.ReLU(), nn.Linear(in_features=32, out_features=64), nn.ReLU(), nn.Linear(in_features=64, out_features=1)) 注: nn.functional.relu(x) 等于 nn.ReLU()(x) 如前面所说的,层数越多隐藏值数量越多模型就越强大,但需要更长的训练时间并且更容易发生过拟合问题,实际操作时我们可以选择一个比较小的模型,再按需要增加层数和隐藏值个数。 三层线性模型的计算图可以表现如下,以后这个系列在讲解其他模型的时候也会使用相同形式的图表表示模型的计算路径: 实例 - 根据码农条件求工资我们已经了解到如何创建多层线性模型,现在可以试试解决比较实际的问题了,对于大部分码农来说最实际的问题就是每个月能拿多少工资,那就来建立一个根据码农的条件,预测可以拿到多少工资的模型吧。 以下是从某个地方秘密收集回来的码农条件和工资数据(其实是按某种规律随机生成出来的,不是实际数据): 年龄,性别,工作经验,Java,NET,JS,CSS,HTML,工资29,0,0,1,2,2,1,4,1250022,0,2,2,3,1,2,5,1550024,0,4,1,2,1,1,2,1600035,0,6,3,3,0,1,0,1950045,0,18,0,5,2,0,5,1700024,0,2,0,0,0,1,1,1350023,1,2,2,3,1,1,0,1050041,0,16,2,5,5,2,0,1650050,0,18,0,5,0,5,2,1650020,0,0,0,5,2,0,1,1250026,0,6,1,5,5,1,1,2700046,0,12,0,5,4,4,2,1250026,0,6,1,5,3,1,1,2350040,0,9,0,0,1,0,1,1750041,0,20,3,5,3,3,5,2050026,0,4,0,1,2,4,0,1850042,0,18,5,0,0,2,5,1850021,0,1,1,0,1,2,0,1200026,0,1,0,0,0,0,2,12500完整数据有 50000 条,可以从 https://github.com/303248153/BlogArchive/tree/master/ml-03/salary.csv 下载。 每个码农有以下条件: 年龄性别 (0: 男性, 1: 女性)工作经验年数 (仅限互联网行业)Java 编码熟练程度 (0 ~ 5)NET 编码熟练程度 (0 ~ 5)JS 编码熟练程度 (0 ~ 5)CSS 编码熟练程度 (0 ~ 5)HTML 编码熟练程度 (0 ~ 5)也就是有 8 个输入,1 个输出 (工资),我们可以建立三层线性模型: 第一层接收 8 个输入返回 100 个隐藏值第二层接收 100 个隐藏值返回 50 个隐藏值第三层接收 50 个隐藏值返回 1 个输出写成代码如下 (这里使用了 pandas 类库读取 csv,使用 pip3 install pandas 即可安装): 引用 pytorch 和 pandas import pandasimport torchfrom torch import nn 定义模型 class MyModel(nn.Module): def __init__(self): super().__init__() self.layer1 = nn.Linear(in_features=8, out_features=100) self.layer2 = nn.Linear(in_features=100, out_features=50) self.layer3 = nn.Linear(in_features=50, out_features=1) def forward(self, x): hidden1 = nn.functional.relu(self.layer1(x)) hidden2 = nn.functional.relu(self.layer2(hidden1)) y = self.layer3(hidden2) return y 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数 这是为了让训练过程可重现,你也可以选择不这样做 torch.random.manual_seed(0) 创建模型实例 model = MyModel() 创建损失计算器 loss_function = torch.nn.MSELoss() 创建参数调整器 optimizer = torch.optim.SGD(model.parameters(), lr=0.0000001) 从 csv 读取原始数据集 df = pandas.read_csv('salary.csv')dataset_tensor = torch.tensor(df.values, dtype=torch.float) 切分训练集 (60%),验证集 (20%) 和测试集 (20%) random_indices = torch.randperm(dataset_tensor.shape[0])traning_indices = random_indices[:int(len(random_indices)*0.6)]validating_indices = random_indices[int(len(random_indices)0.6):int(len(random_indices)0.8):]testing_indices = random_indices[int(len(random_indices)*0.8):]traning_set_x = dataset_tensortraning_indicestraning_set_y = dataset_tensortraning_indicesvalidating_set_x = dataset_tensorvalidating_indicesvalidating_set_y = dataset_tensorvalidating_indicestesting_set_x = dataset_tensortesting_indicestesting_set_y = dataset_tensortesting_indices 开始训练过程 for epoch in range(1, 1000): print(f"epoch: {epoch}") # 根据训练集训练并修改参数 # 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropout model.train() for batch in range(0, traning_set_x.shape[0], 100): # 切分批次,一次只计算 100 组数据 batch_x = traning_set_x[batch:batch+100] batch_y = traning_set_y[batch:batch+100] # 计算预测值 predicted = model(batch_x) # 计算损失 loss = loss_function(predicted, batch_y) # 从损失自动微分求导函数值 loss.backward() # 使用参数调整器调整参数 optimizer.step() # 清空导函数值 optimizer.zero_grad() # 检查验证集 # 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropout model.eval() predicted = model(validating_set_x) validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).mean() print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}") print(f"validating accuracy: {validating_accuracy}") 检查测试集 predicted = model(testing_set_x)testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).mean()print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")print(f"testing accuracy: {testing_accuracy}") 手动输入数据预测输出 while True: try: print("enter input:") r = list(map(float, input().split(","))) x = torch.tensor(r).view(1, len(r)) print(model(x)[0,0].item()) except Exception as e: print("error:", e) 输出如下,可以看到最后没有参与训练的验证集的正确度达到了 93.3% 测试集的正确度达到了 93.1%,模型成功的摸索出了某种规律: epoch: 1validating x: tensor([[42., 0., 16., ..., 5., 5., 5.], [28., 0., 0., ..., 4., 0., 0.], [23., 0., 3., ..., 0., 1., 1.], ..., [44., 1., 15., ..., 2., 0., 2.], [30., 0., 1., ..., 1., 1., 2.], [50., 1., 18., ..., 5., 5., 2.]]), y: tensor([[24500.], [12500.], [17500.], ..., [10500.], [15000.], [16000.]]), predicted: tensor([[27604.2578], [15934.7607], [14536.8984], ..., [23678.5547], [18189.6953], [29968.8789]], grad_fn=<AddmmBackward>) validating accuracy: 0.661293625831604epoch: 2validating x: tensor([[42., 0., 16., ..., 5., 5., 5.], [28., 0., 0., ..., 4., 0., 0.], [23., 0., 3., ..., 0., 1., 1.], ..., [44., 1., 15., ..., 2., 0., 2.], [30., 0., 1., ..., 1., 1., 2.], [50., 1., 18., ..., 5., 5., 2.]]), y: tensor([[24500.], [12500.], [17500.], ..., [10500.], [15000.], [16000.]]), predicted: tensor([[29718.2441], [15790.3799], [15312.5791], ..., [23395.9668], [18672.0234], [31012.4062]], grad_fn=<AddmmBackward>) validating accuracy: 0.6694601774215698 省略途中输出 epoch: 999validating x: tensor([[42., 0., 16., ..., 5., 5., 5.], [28., 0., 0., ..., 4., 0., 0.], [23., 0., 3., ..., 0., 1., 1.], ..., [44., 1., 15., ..., 2., 0., 2.], [30., 0., 1., ..., 1., 1., 2.], [50., 1., 18., ..., 5., 5., 2.]]), y: tensor([[24500.], [12500.], [17500.], ..., [10500.], [15000.], [16000.]]), predicted: tensor([[22978.7656], [13050.8018], [18396.5176], ..., [11449.5059], [14791.2969], [16635.2578]], grad_fn=<AddmmBackward>) validating accuracy: 0.9311849474906921testing x: tensor([[48., 1., 18., ..., 5., 0., 5.], [22., 1., 2., ..., 2., 1., 2.], [24., 0., 1., ..., 3., 2., 0.], ..., [24., 0., 4., ..., 0., 1., 1.], [39., 0., 0., ..., 0., 5., 5.], [36., 0., 5., ..., 3., 0., 3.]]), y: tensor([[14000.], [10500.], [13000.], ..., [15500.], [12000.], [19000.]]), predicted: tensor([[15481.9062], [11011.7266], [12192.7949], ..., [16219.3027], [11074.0420], [20305.3516]], grad_fn=<AddmmBackward>) testing accuracy: 0.9330180883407593enter input:最后我们手动输入码农条件可以得出预测输出 (35 岁男 10 年经验 Java 5 NET 2 JS 1 CSS 1 HTML 2 大约可拿 26k ): enter input:35,0,10,5,2,1,1,226790.982421875虽然训练成功了,但以上代码还有几个问题: 学习比率设置的非常低 (0.0000001),这是因为我们没有对数据进行正规化处理总是会固定训练 1000 次,不能像第一篇文章提到过的那样自动检测哪一次的正确度最高,没有考虑过拟合问题不能把训练结果保存到硬盘,每次运行都需要从头训练需要把所有数据一次性读取到内存中,数据过多时内存可能不够用没有提供一个使用训练好的模型的接口这些问题都会在下篇文章一个个解决。 写在最后在这一篇我们终于看到怎样应用机器学习到更复杂的数据中,但路还有很长。 原文地址https://www.cnblogs.com/zkweb/p/12761743.html
架构师修炼之微服务部署 - Docker简介 Docker简介Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器或Windows 机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker动手实验平台:Play with Docker。 Docker 最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的一次革新,并于2013 年 3 月以 Apache 2.0 授权协议开源,主要项目代码在GitHub上进行维护。Docker 项目后来还加入了 Linux 基金会,并成立推动开放容器联盟(OCI)。 Docker 自开源后受到广泛的关注和讨论,至今其 GitHub 项目已经超过 4 万 6 千个星标和一万多个 fork。甚至由于 Docker 项目的火爆,在 2013 年底,dotCloud 公司决定改名为 Docker。Docker 最初是在 Ubuntu 12.04 上开发实现的;Red Hat 则从 RHEL 6.5 开始对 Docker 进行支持;Google 也在其 PaaS 产品中广泛应用 Docker。 Docker 使用 Google 公司推出的Go 语言进行开发实现,基于 Linux 内核的cgroup,namespace,以及AUFS类的Union FS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的libcontainer,从 1.11 开始,则进一步演进为使用runC和containerd。 Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。 容器(Containers) 虚拟主机(Virtual Machines) 容器内应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。 虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程。缺点:消耗硬件资源;配置和启动都慢。内部架构 组成部分: Docker客户端Docker服务端daemon 守护进程image 镜像container 容器Docker镜像仓库概念Docker客户端发布操作指令给Docker服务端进行容器与镜像操作,类似Xshell,Teraterm。 Daemon守护进程daemon在服务端宿主主机后台运行,接受来自客户的请求,并处理这些请求(创建、运行、分发容器)。 DockerFile文件一个用来构建镜像的文本文件,包含了一条条构建镜像所需要的指令和说明。 镜像(image)创建容器的模板,一般程序员通过编译DockerFile文件创建。 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。无法对镜像进行修改。--简书 容器(Container)镜像实例-标准化的应用,可以进入容器进行修改。一个镜像可以创建多个独立运行的容器。 容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的root文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。--简书 仓库(Repository)集中化存储镜像的地方,一般使用Docker Registry构建自己的私有仓库。而官方的DockerHub提供操作系统、数据库、web服务或者其他公开的镜像。 镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。--简书 镜像容器创建实操(Windows版)创建Asp.netCore项目因为安装VS2019太费时间,所有通过SDK命令来创建项目。 安装Asp.netCore开发环境因为需要使用模板创建项目,所有需要进入官网(https://dotnet.microsoft.com/download)下载安装.NETCore SDK 3.1 确认是否安装成功启动cmd命令窗口输入以下命令dotnet --version如果输出为 3.1 打头,代表安装成功启动cmd窗口,cd到作业目录。输入以下命令创建项目。dotnet new webapp -o aspnetcoreapp进入项目目录,运行项目。D:docker>cd aspnetcoreappD:dockeraspnetcoreapp>dotnet restoreD:dockeraspnetcoreappaspnetcoreapp.csproj 的还原在 65.58 ms 内完成。 D:dockeraspnetcoreapp>dotnet runinfo: Microsoft.Hosting.Lifetime[0] Now listening on: https://localhost:5001 info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: D:\docker\aspnetcoreapp 进入浏览器,输入地址:https://localhost:5001 或者 http://localhost:5000。如果显示了Welcome,代表创建成功。 创建DockerFile在作业目录,创建无后缀的文件:DockerFile。内容如下: 备注:如果使用VS2019可以通过右键项目,选择“Docker支持”自动生成此文件。 FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS baseWORKDIR /appEXPOSE 80EXPOSE 443 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS buildWORKDIR /srcCOPY ["aspnetcoreapp/aspnetcoreapp.csproj", "aspnetcoreapp/"]RUN dotnet restore "aspnetcoreapp/aspnetcoreapp.csproj"COPY . .WORKDIR "/src/aspnetcoreapp"RUN dotnet build "aspnetcoreapp.csproj" -c Release -o /app/build FROM build AS publishRUN dotnet publish "aspnetcoreapp.csproj" -c Release -o /app/publish FROM base AS finalWORKDIR /appCOPY --from=publish /app/publish .ENTRYPOINT ["dotnet", "aspnetcoreapp.dll"]创建镜像安装Docker Desktop for windows.https://www.docker.com/products/docker-desktop进入CMD窗口,查看安装版本。(因为是本地演示,必须保证Client和Server都存在)D:docker>docker versionClient: Docker Engine - CommunityVersion: 19.03.8API version: 1.40Go version: go1.12.17Git commit: afacb8bBuilt: Wed Mar 11 01:23:10 2020OS/Arch: windows/amd64Experimental: false Server: Docker Engine - CommunityEngine:Version: 19.03.8API version: 1.40 (minimum version 1.12)Go version: go1.12.17Git commit: afacb8bBuilt: Wed Mar 11 01:29:16 2020OS/Arch: linux/amd64Experimental: falsecontainerd:Version: v1.2.13GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429runc:Version: 1.0.0-rc10GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dddocker-init:Version: 0.18.0GitCommit: fec3683CMD窗口进入工作目录,通过DockerFile创建镜像备注:因为我有缓存Aspnet3.1和sdk3.1,所以没有出现下载信息。 D:docker>dir驱动器 D 中的卷是 Soft卷的序列号是 CC3B-E6AD D:docker 的目录 2020/04/22 15:46 .2020/04/22 15:46 ..2020/04/22 13:08 aspnetcoreapp2020/04/22 15:40 615 DockerFile 1 个文件 615 字节 3 个目录 86,923,542,528 可用字节 D:docker>docker build -t aspnetcore .Sending build context to Docker daemon 6.373MBStep 1/17 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base---> 0661f995e7dbStep 2/17 : WORKDIR /app---> Running in 361e77bdad90Removing intermediate container 361e77bdad90---> add07effc24aStep 3/17 : EXPOSE 80---> Running in 9a384d1bd5e4Removing intermediate container 9a384d1bd5e4---> bea582d752fcStep 4/17 : EXPOSE 443---> Running in 4690332ca309Removing intermediate container 4690332ca309---> 47da2ca7d6ceStep 5/17 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build---> 3edbb65c61daStep 6/17 : WORKDIR /src---> Using cache---> 5464ff3ac1fbStep 7/17 : COPY ["aspnetcoreapp/aspnetcoreapp.csproj", "aspnetcoreapp/"]---> Using cache---> d9cbfaaf5a1aStep 8/17 : RUN dotnet restore "aspnetcoreapp/aspnetcoreapp.csproj"---> Using cache---> f3fc708d4809Step 9/17 : COPY . .---> Using cache---> c30b51b049a7Step 10/17 : WORKDIR "/src/aspnetcoreapp"---> Using cache---> 089b9f9f7a27Step 11/17 : RUN dotnet build "aspnetcoreapp.csproj" -c Release -o /app/build---> Using cache---> 01728a06901eStep 12/17 : FROM build AS publish---> 01728a06901eStep 13/17 : RUN dotnet publish "aspnetcoreapp.csproj" -c Release -o /app/publish---> Using cache---> bf5986a7c2e4Step 14/17 : FROM base AS final---> 47da2ca7d6ceStep 15/17 : WORKDIR /app---> Running in 098789c69783Removing intermediate container 098789c69783---> 373b88783227Step 16/17 : COPY --from=publish /app/publish .---> 2a00a6670e90Step 17/17 : ENTRYPOINT ["dotnet", "aspnetcoreapp.dll"]---> Running in 94f2bb2fae82Removing intermediate container 94f2bb2fae82---> 73ae1ad12839Successfully built 73ae1ad12839Successfully tagged aspnetcore:latestSECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories. D:docker>docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEaspnetcore latest 73ae1ad12839 32 seconds ago 212MB 创建容器先确认容器列表,然后使用镜像名称创建容器。-d:表示后台运行;-P:表示自动分配端口(-p 8080:80 形式自定义端口) D:docker>docker container lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES D:docker>docker run -d -P aspnetcore4129eb2516af33ea9346c8d6eefec6e1d5d83ec728ab3ddb093a4a3610dd3001 D:docker>docker container lsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES4129eb2516af aspnetcore "dotnet aspnetcoreap…" 4 seconds ago Up 3 seconds 0.0.0.0:32771->80/tcp, 0.0.0.0:32770->443/tcp elegant_chebyshev执行结果打开浏览器,输入地址:http://localhost:32771/ 原文地址https://www.cnblogs.com/lixiaobin/p/dockerdescription.html
java nio消息半包、粘包解决方案 问题背景NIO是面向缓冲区进行通信的,不是面向流的。我们都知道,既然是缓冲区,那它一定存在一个固定大小。这样一来通常会遇到两个问题: 消息粘包:当缓冲区足够大,由于网络不稳定种种原因,可能会有多条消息从通道读入缓冲区,此时如果无法分清数据包之间的界限,就会导致粘包问题;消息不完整:若消息没有接收完,缓冲区就被填满了,会导致从缓冲区取出的消息不完整,即半包的现象。介绍这个问题之前,务必要提一下我代码整体架构。代码参见GitHub仓库 https://github.com/CuriousLei/smyl-im 在这个项目中,我的NIO核心库设计思路流程图如下所示 介绍: 服务端为每一个连接上的客户端建立一个Connector对象,为其提供IO服务;ioArgs对象内部实例域引用了缓冲区buffer,作为直接与channel进行数据交互的缓冲区;两个线程池,分别操控ioArgs进行读和写操作;connector与ioArgs关系:(1)输入,线程池处理读事件,数据写入ioArgs,并回调给connector;(2)输出,connector将数据写入ioArgs,将ioArgs传入Runnable对象,供线程池处理;两个selector线程,分别监听channel的读和写事件。事件就绪,则触发线程池工作。思路光这样实现,必然会有粘包、半包问题。要重现这两个问题也很简单。 ioArgs中把缓冲区设置小一点,发送一条大于该长度的数据,服务端会当成两条消息读取,即消息不完整;在线程代码中,加一个Thread.sleep()延时等待,客户端连续发几条消息(总长度小于缓冲区大小),也可以重现粘包现象。这个问题实质上是消息体与缓冲区数据不一一对应导致的。那么,如何解决呢? 固定头部方案可以采用固定头部方案来解决,头部设置四个字节,存储一个int值,记录后面数据的长度。以此来标记一个消息体。 读取数据时,根据头部的长度信息,按序读取ioArgs缓冲区中的数据,若没有达到长度要求,继续读下一个ioArgs。这样自然不会出现粘包、半包问题。输出数据时,也采用同样的机制封装数据,首部四个字节记录长度。我的工程项目中,客户端和服务端共用一个nio核心包,即niohdl,可保证收发数据格式一致。 设计方案要实现以上设想,必须在connector和ioArgs之间加一层Dispatcher类,用于处理消息体与缓冲区之间的转化关系(消息体取个名字:Packet)。根据输入和输出的不同,分别叫ReceiveDispatcher和SendDispatcher。即通过它们来操作Packet与ioArgs之间的转化。 Packet定义这个消息体,继承关系如下图所示: Packet是基类,代码如下: package cn.buptleida.niohdl.core;import java.io.Closeable;import java.io.IOException;/** 公共的数据封装 提供了类型以及基本的长度的定义*/ public class Packet implements Closeable { protected byte type; protected int length; public byte type(){ return type; } public int length(){ return length; } @Override public void close() throws IOException { } }SendPacket和ReceivePacket分别代表发送消息体和接收消息体。StringReceivePacket和StringSendPacket代表字符串类的消息,因为本次实践只限于字符串消息的收发,今后可能有文件之类的,有待扩展。 代码中必然会涉及到字节数组的操作,所以,以StringSendPacket为例,需要提供将String转化为byte[]的方法。代码如下所示: package cn.buptleida.niohdl.box;import cn.buptleida.niohdl.core.SendPacket; public class StringSendPacket extends SendPacket { private final byte[] bytes; public StringSendPacket(String msg) { this.bytes = msg.getBytes(); this.length = bytes.length;//父类中的实例域 } @Override public byte[] bytes() { return bytes; } }SendDispatcher在connector对象的实例域中会引用一个SendDispatcher对象。发送数据时,会通过SendDispatcher中的方法对数据进行封装和处理。其大致的关系图如下所示: SendDispatcher中设置任务队列Queue queue,需要发送消息时,connector将消息写入sendPacket,并存入队列queue,执行出队。用packetTemp变量引用出队的元素,将四字节的长度信息和packetTemp写入ioArgs的缓冲区中,发送完毕之后,再判断packetTemp是否完整写出(使用position和total指针标记、判断),决定继续输出packetTemp的内容,还是开始下一轮出队。这个过程的程序框图如下所示: 在代码中,SendDispatcher实际上是一个接口,我用AsyncSendDispatcher实现此接口,代码如下: package cn.buptleida.niohdl.impl.async; import cn.buptleida.niohdl.core.*;import cn.buptleida.utils.CloseUtil; import java.io.IOException;import java.util.Queue;import java.util.concurrent.ConcurrentLinkedDeque;import java.util.concurrent.atomic.AtomicBoolean; public class AsyncSendDispatcher implements SendDispatcher { private final AtomicBoolean isClosed = new AtomicBoolean(false); private Sender sender; private Queue<SendPacket> queue = new ConcurrentLinkedDeque<>(); private AtomicBoolean isSending = new AtomicBoolean(); private ioArgs ioArgs = new ioArgs(); private SendPacket packetTemp; //当前发送的packet大小以及进度 private int total; private int position; public AsyncSendDispatcher(Sender sender) { this.sender = sender; } /** * connector将数据封装进packet后,调用这个方法 * @param packet */ @Override public void send(SendPacket packet) { queue.offer(packet);//将数据放进队列中 if (isSending.compareAndSet(false, true)) { sendNextPacket(); } } @Override public void cancel(SendPacket packet) { } /** * 从队列中取数据 * @return */ private SendPacket takePacket() { SendPacket packet = queue.poll(); if (packet != null && packet.isCanceled()) { //已经取消不用发送 return takePacket(); } return packet; } private void sendNextPacket() { SendPacket temp = packetTemp; if (temp != null) { CloseUtil.close(temp); } SendPacket packet = packetTemp = takePacket(); if (packet == null) { //队列为空,取消发送状态 isSending.set(false); return; } total = packet.length(); position = 0; sendCurrentPacket(); } private void sendCurrentPacket() { ioArgs args = ioArgs; args.startWriting();//将ioArgs缓冲区中的指针设置好 if (position >= total) { sendNextPacket(); return; } else if (position == 0) { //首包,需要携带长度信息 args.writeLength(total); } byte[] bytes = packetTemp.bytes(); //把bytes的数据写入到IoArgs中 int count = args.readFrom(bytes, position); position += count; //完成封装 args.finishWriting();//flip()操作 //向通道注册OP_write,将Args附加到runnable中;selector线程监听到就绪即可触发线程池进行消息发送 try { sender.sendAsync(args, ioArgsEventListener); } catch (IOException e) { closeAndNotify(); } } private void closeAndNotify() { CloseUtil.close(this); } @Override public void close(){ if (isClosed.compareAndSet(false, true)) { isSending.set(false); SendPacket packet = packetTemp; if (packet != null) { packetTemp = null; CloseUtil.close(packet); } } } /** * 接收回调,来自writeHandler输出线程 */ private ioArgs.IoArgsEventListener ioArgsEventListener = new ioArgs.IoArgsEventListener() { @Override public void onStarted(ioArgs args) { } @Override public void onCompleted(ioArgs args) { //继续发送当前包packetTemp,因为可能一个包没发完 sendCurrentPacket(); } }; } ReceiveDispatcher同样,ReceiveDispatcher也是一个接口,代码中用AsyncReceiveDispatcher实现。在connector对象的实例域中会引用一个AsyncReceiveDispatcher对象。接收数据时,会通过ReceiveDispatcher中的方法对接收到的数据进行拆包处理。其大致的关系图如下所示: 每一个消息体的首部会有一个四字节的int字段,代表消息的长度值,按照这个长度来进行读取。如若一个ioArgs未满足这个长度,就读取下一个ioArgs,保证数据包的完整性。这个流程就不画程序框图了,偷个懒hhhh。其实看下面代码注释已经很清晰了,容易理解。 AsyncReceiveDispatcher的代码如下所示: package cn.buptleida.niohdl.impl.async; import cn.buptleida.niohdl.box.StringReceivePacket;import cn.buptleida.niohdl.core.ReceiveDispatcher;import cn.buptleida.niohdl.core.ReceivePacket;import cn.buptleida.niohdl.core.Receiver;import cn.buptleida.niohdl.core.ioArgs;import cn.buptleida.utils.CloseUtil; import java.io.IOException;import java.util.concurrent.atomic.AtomicBoolean; public class AsyncReceiveDispatcher implements ReceiveDispatcher { private final AtomicBoolean isClosed = new AtomicBoolean(false); private final Receiver receiver; private final ReceivePacketCallback callback; private ioArgs args = new ioArgs(); private ReceivePacket packetTemp; private byte[] buffer; private int total; private int position; public AsyncReceiveDispatcher(Receiver receiver, ReceivePacketCallback callback) { this.receiver = receiver; this.receiver.setReceiveListener(ioArgsEventListener); this.callback = callback; } /** * connector中调用该方法进行 */ @Override public void start() { registerReceive(); } private void registerReceive() { try { receiver.receiveAsync(args); } catch (IOException e) { closeAndNotify(); } } private void closeAndNotify() { CloseUtil.close(this); } @Override public void stop() { } @Override public void close() throws IOException { if(isClosed.compareAndSet(false,true)){ ReceivePacket packet = packetTemp; if(packet!=null){ packetTemp = null; CloseUtil.close(packet); } } } /** * 回调方法,从readHandler输入线程中回调 */ private ioArgs.IoArgsEventListener ioArgsEventListener = new ioArgs.IoArgsEventListener() { @Override public void onStarted(ioArgs args) { int receiveSize; if (packetTemp == null) { receiveSize = 4; } else { receiveSize = Math.min(total - position, args.capacity()); } //设置接受数据大小 args.setLimit(receiveSize); } @Override public void onCompleted(ioArgs args) { assemblePacket(args); //继续接受下一条数据,因为可能同一个消息可能分隔在两份IoArgs中 registerReceive(); } }; /** * 解析数据到packet * @param args */ private void assemblePacket(ioArgs args) { if (packetTemp == null) { int length = args.readLength(); packetTemp = new StringReceivePacket(length); buffer = new byte[length]; total = length; position = 0; } //将args中的数据写进外面buffer中 int count = args.writeTo(buffer,0); if(count>0){ //将数据存进StringReceivePacket的buffer当中 packetTemp.save(buffer,count); position+=count; if(position == total){ completePacket(); packetTemp = null; } } } private void completePacket() { ReceivePacket packet = this.packetTemp; CloseUtil.close(packet); callback.onReceivePacketCompleted(packet); } }总结其实粘包、半包的解决方案并没有什么奥秘,单纯地复杂而已。方法核心就是自定义一个消息体Packet,完成Packet中的byte数组与缓冲区数组之间的复制转化即可。当然,position、limit等等指针的辅助很重要。 总结这个博客,也是将目前为止的工作进行梳理和记录。我将通过smyl-im这个项目来持续学习+实践。因为之前学习过程中有很多零碎的知识点,都躺在我的有道云笔记里,感觉没必要总结成博客。本次博客讲的内容刚好是一个成体系的东西,正好可以将这个项目背景带出来,后续的博客就可以在这基础上衍生拓展了。 原文地址https://www.cnblogs.com/buptleida/p/12732288.html
C#多线程(4):进程同步Mutex类 目录Mutex 类构造函数和方法系统只能运行一个程序的实例解释一下上面的示例接替运行进程同步示例另外Mutex 类Mutex 中文为互斥,Mutex 类叫做互斥锁。它还可用于进程间同步的同步基元。 Mutex 跟 lock 相似,但是 Mutex 支持多个进程。Mutex 大约比 lock 慢 20 倍。 互斥锁(Mutex),用于多线程中防止两条线程同时对一个公共资源进行读写的机制。 Windows 操作系统中,Mutex 同步对象有两个状态: signaled:未被任何对象拥有;nonsignaled:被一个线程拥有;Mutex 只能在获得锁的线程中,释放锁。 构造函数和方法Mutex 类其构造函数如下: 构造函数 说明Mutex() 使用默认属性初始化 Mutex类的新实例。Mutex(Boolean) 使用 Boolean 值(指示调用线程是否应具有互斥体的初始所有权)初始化 Mutex 类的新实例。Mutex(Boolean, String) 使用 Boolean 值(指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称)初始化 Mutex 类的新实例。Mutex(Boolean, String, Boolean) 使用可指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称的 Boolean 值和当线程返回时可指示调用线程是否已赋予互斥体的初始所有权的 Boolean 值初始化 Mutex 类的新实例。Mutex 对于进程同步有所帮助,例如其应用场景主要是控制系统只能运行一个此程序的实例。 Mutex 构造函数中的 String类型参数 叫做互斥量而互斥量是全局的操作系统对象。Mutex 只要考虑实现进程间的同步,它会耗费比较多的资源,进程内请考虑 Monitor/lock。Mutex 的常用方法如下: 方法 说明Close() 释放由当前 WaitHandle 占用的所有资源。Dispose() 释放由 WaitHandle 类的当前实例占用的所有资源。OpenExisting(String) 打开指定的已命名的互斥体(如果已经存在)。ReleaseMutex() 释放 Mutex一次。TryOpenExisting(String, Mutex) 打开指定的已命名的互斥体(如果已经存在),并返回指示操作是否成功的值。WaitOne() 阻止当前线程,直到当前 WaitHandle 收到信号。WaitOne(Int32) 阻止当前线程,直到当前 WaitHandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。WaitOne(Int32, Boolean) 阻止当前线程,直到当前的 WaitHandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。WaitOne(TimeSpan) 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。WaitOne(TimeSpan, Boolean) 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。关于 Mutex 类,我们可以先通过几个示例去了解它。 系统只能运行一个程序的实例下面是一个示例,用于控制系统只能运行一个此程序的实例,不允许同时启动多次。 class Program { // 第一个程序 const string name = "www.whuanle.cn"; private static Mutex m; static void Main(string[] args) { // 本程序是否是 Mutex 的拥有者 bool firstInstance; m = new Mutex(false,name,out firstInstance); if (!firstInstance) { Console.WriteLine("程序已在运行!按下回车键退出!"); Console.ReadKey(); return; } Console.WriteLine("程序已经启动"); Console.WriteLine("按下回车键退出运行"); Console.ReadKey(); m.ReleaseMutex(); m.Close(); return; } } 上面的代码中,有些地方前面没有讲,没关系,我们运行一下生成的程序先。 解释一下上面的示例Mutex 的工作原理: 当两个或两个以上的线程同时访问共享资源时,操作系统需要一个同步机制来确保每次只有一个线程使用资源。 Mutex 是一种同步基元,Mutex 仅向一个线程授予独占访问共享资源的权限。这个权限依据就是 互斥体,当一个线程获取到互斥体后,其它线程也在试图获取互斥体时,就会被挂起(阻塞),直到第一个线程释放互斥体。 对应我们上一个代码示例中,实例化 Mutex 类的构造函数如下: m = new Mutex(false,name,out firstInstance);其构造函数原型如下: public Mutex (bool initiallyOwned, string name, out bool createdNew); 前面我们提出过,Mutex 对象有两种状态,signaled 和 nonsignaled。 通过 new 来实例化 Mutex 类,会检查系统中此互斥量 name 是否已经被使用,如果没有被使用,则会创建 name 互斥量并且此线程拥有此互斥量的使用权;此时 createdNew == true。 那么 initiallyOwned ,它的作用是是否允许线程是否能够获取到此互斥量的初始化所有权。因为我们希望只有一个程序能够在后台运行,因此我们要设置为 false。 驱动开发中关于Mutex :https://docs.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-mutex-objects 对了, Mutex 的 参数中,name 是非常有讲究的。 在运行终端服务的服务器上,命名系统 mutex 可以有两个级别的可见性。 如果其名称以前缀 "Global" 开头,则 mutex 在所有终端服务器会话中可见。如果其名称以前缀 "Local" 开头,则 mutex 仅在创建它的终端服务器会话中可见。 在这种情况下,可以在服务器上的其他每个终端服务器会话中存在具有相同名称的单独 mutex。如果在创建已命名的 mutex 时未指定前缀,则采用前缀 "Local"。 在终端服务器会话中,两个互斥体的名称只是它们的前缀不同,它们都是对终端服务器会话中的所有进程都可见。 也就是说,前缀名称 "Global" 和 "Local" 描述互斥体名称相对于终端服务器会话的作用域,而不是相对于进程。 请参考: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex?view=netcore-3.1#methods https://www.cnblogs.com/suntp/p/8258488.html 接替运行这里要实现,当同时点击一个程序时,只能有一个实例A可以运行,其它实例进入等待队列,等待A运行完毕后,然后继续运行队列中的下一个实例。 我们将每个程序比作一个人,模拟一个厕所坑位,每次只能有一个人上厕所,其他人需要排队等候。 使用 WaitOne() 方法来等待别的进程释放互斥量,即模拟排队;ReleaseMutex() 方法解除对坑位的占用。 class Program { // 第一个程序 const string name = "www.whuanle.cn"; private static Mutex m; static void Main(string[] args) { // wc 还有没有位置 bool firstInstance; m = new Mutex(true,name,out firstInstance); // 已经有人在上wc if (!firstInstance) { // 等待运行的实例退出,此进程才能运行。 Console.WriteLine("排队等待"); m.WaitOne(); GoWC(); return; } GoWC(); return; } private static void GoWC() { Console.WriteLine(" 开始上wc"); Thread.Sleep(1000); Console.WriteLine(" 开门"); Thread.Sleep(1000); Console.WriteLine(" 关门"); Thread.Sleep(1000); Console.WriteLine(" xxx"); Thread.Sleep(1000); Console.WriteLine(" 开门"); Thread.Sleep(1000); Console.WriteLine(" 离开wc"); m.ReleaseMutex(); Thread.Sleep(1000); Console.WriteLine(" 洗手"); } } 此时,我们使用了 m = new Mutex(true,name,out firstInstance); 一个程序结束后,要允许其它线程能够创建 Mutex 对象获取互斥量,需要将构造函数的第一个参数设置为 true。 你也可以改成 false,看看会报什么异常。 你可以使用 WaitOne(Int32) 来设置等待时间,单位是毫秒,超过这个时间就不排队了,去别的地方上厕所。 为了避免出现问题,请考虑在 finally 块中执行 m.ReleaseMutex()。 进程同步示例这里我们实现一个这样的场景: 父进程 Parant 启动子进程 Children ,等待子进程 Children 执行完毕,子进程退出,父进程退出。 新建一个 .NET Core 控制台项目,名称为 Children,其 Progarm 中的代码如下 using System;using System.Threading; namespace Children{ class Program { const string name = "进程同步示例"; private static Mutex m; static void Main(string[] args) { Console.WriteLine("子进程被启动..."); bool firstInstance; // 子进程创建互斥体 m = new Mutex(true, name, out firstInstance); // 按照我们设计的程序,创建一定是成功的 if (firstInstance) { Console.WriteLine("子线程执行任务"); DoWork(); Console.WriteLine("子线程任务完成"); // 释放互斥体 m.ReleaseMutex(); // 结束程序 return; } else { Console.WriteLine("莫名其妙的异常,直接退出"); } } private static void DoWork() { for (int i = 0; i < 5; i++) { Console.WriteLine("子线程工作中"); Thread.Sleep(TimeSpan.FromSeconds(1)); } } } } 然后发布或生成项目,打开程序文件位置,复制线程文件路径。创建一个新项目,名为 Parant 的 .NET Core 控制台,其 Program 中的代码如下: using System;using System.Diagnostics;using System.Threading; namespace Parant{ class Program { const string name = "进程同步示例"; private static Mutex m; static void Main(string[] args) { // 晚一些再执行,我录屏要对正窗口位置 Thread.Sleep(TimeSpan.FromSeconds(3)); Console.WriteLine("父进程启动!"); new Thread(() => { // 启动子进程 Process process = new Process(); process.StartInfo.UseShellExecute = true; process.StartInfo.CreateNoWindow = false; process.StartInfo.WorkingDirectory = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1"; process.StartInfo.FileName = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1\Children.exe"; process.Start(); process.WaitForExit(); }).Start(); // 子进程启动需要一点时间 Thread.Sleep(TimeSpan.FromSeconds(1)); // 获取互斥体 bool firstInstance; m = new Mutex(true, name, out firstInstance); // 说明子进程还在运行 if (!firstInstance) { // 等待子进程运行结束 Console.WriteLine("等待子进程运行结束"); m.WaitOne(); Console.WriteLine("子进程运行结束,程序将在3秒后自动退出"); m.ReleaseMutex(); Thread.Sleep(TimeSpan.FromSeconds(3)); return; } } } } 请将 Children 项目的程序文件路径,替换到 Parant 项目启动子进程的那部分字符串中。 然后启动 Parant.exe,可以观察到如下图的运行过程: 另外构造函数中,如果为 name 指定 null 或空字符串,则将创建一个本地 Mutex 对象,只会在进程内有效。 Mutex 有些使用方法比较隐晦,可以参考 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.-ctor?view=netcore-3.1#System_Threading_Mutex__ctor_System_Boolean_ 另外打开互斥体,请参考 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.openexisting?view=netcore-3.1 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.tryopenexisting?view=netcore-3.1 到目前为止,我们学习了排他锁 lock、Monitor、Mutex。下一篇我们将来学习非排他锁定结构的Semaphore和SemaphoreSlim 。 原文地址https://www.cnblogs.com/whuanle/p/12726724.html
Redis对象——有序集合(ZSet) 有序集合类型 (Sorted Set或ZSet) 相比于集合类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。 一、内部实现#有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。 当数据比较少时,有序集合使用的是 ziplist 存储的,有序集合使用 ziplist 格式存储必须满足以下两个条件: 有序集合保存的元素个数要小于 128 个;有序集合保存的所有元素成员的长度都必须小于 64 字节。如果不能满足以上两个条件中的任意一个,有序集合将会使用 skiplist 结构进行存储。 有关ziplist 和skiplist 这两种redis底层数据结构的具体实现可以参考我的另外两篇文章。 Redis数据结构——压缩列表 Redis数据结构——跳跃表。 二、常用命令#Redis列表对象常用命令如下表(点击命令可查看命令详细说明)。 命令 说明 时间复杂度BZPOPMAX key [key ...] timeout 从一个或多个排序集中删除并返回得分最高的成员,或阻塞,直到其中一个可用为止 O(log(N))BZPOPMIN key [key ...] timeout 从一个或多个排序集中删除并返回得分最低的成员,或阻塞,直到其中一个可用为止 O(log(N))ZADD key [NXXX] [CH] [INCR] score member [score member ...] 添加到有序set的一个或多个成员,或更新的分数,如果它已经存在 O(log(N))ZCARD key 获取一个排序的集合中的成员数量 O(1)ZCOUNT key min max 返回分数范围内的成员数量 O(log(N))ZINCRBY key increment member 增量的一名成员在排序设置的评分 O(log(N))ZINTERSTORE 相交多个排序集,导致排序的设置存储在一个新的关键 O(NK)+O(Mlog(M))ZLEXCOUNT key min max 返回成员之间的成员数量 O(log(N))ZPOPMAX key [count] 删除并返回排序集中得分最高的成员 O(log(N)*M)ZPOPMIN key [count] 删除并返回排序集中得分最低的成员 O(log(N)*M)ZRANGE key start stop [WITHSCORES] 根据指定的index返回,返回sorted set的成员列表 O(log(N)+M)ZRANGEBYLEX key min max [LIMIT offset count] 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。 O(log(N)+M)ZREVRANGEBYLEX key max min [LIMIT offset count] 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同 O(log(N)+M)ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 返回有序集合中指定分数区间内的成员,分数由低到高排序。 O(log(N)+M)ZRANK key member 确定在排序集合成员的索引 O(log(N))ZREM key member [member ...] 从排序的集合中删除一个或多个成员 O(M*log(N))ZREMRANGEBYLEX key min max 删除名称按字典由低到高排序成员之间所有成员。 O(log(N)+M)ZREMRANGEBYRANK key start stop 在排序设置的所有成员在给定的索引中删除 O(log(N)+M)ZREMRANGEBYSCORE key min max 删除一个排序的设置在给定的分数所有成员 O(log(N)+M)ZREVRANGE key start stop [WITHSCORES] 在排序的设置返回的成员范围,通过索引,下令从分数高到低 O(log(N)+M)ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] 返回有序集合中指定分数区间内的成员,分数由高到低排序。 O(log(N)+M)ZREVRANK key member 确定指数在排序集的成员,下令从分数高到低 O(log(N))ZSCORE key member 获取成员在排序设置相关的比分 O(1)ZUNIONSTORE 添加多个排序集和导致排序的设置存储在一个新的关键 O(N)+O(M log(M))ZSCAN key cursor [MATCH pattern] [COUNT count] 迭代sorted sets里面的元素 O(1)三、使用场景#3.1 排行榜系统#有序集合比较典型的使用场景就是排行榜系统。例如学生成绩的排名。某视频(博客等)网站的用户点赞、播放排名、电商系统中商品的销量排名等。我们以博客点赞为例。 添加用户赞数例如小编Tom发表了一篇博文,并且获得了10个赞。 Copyzadd user:ranking arcticle1 10取消用户赞数这个时候有一个读者又觉得Tom写的不好,又取消了赞,此时需要将文章的赞数从榜单中减去1,可以使用zincrby。 Copyzincrby user:ranking arcticle1 -1查看某篇文章的赞数CopyZSCORE user:ranking arcticle1展示获取赞数最多的十篇文章此功能使用zrevrange命令实现: Copyzrevrangebyrank user:ranking 0 93.2 电话号码(姓名)排序#使用有序集合的ZRANGEBYLEX(点击可查看该命令详细说明)或ZREVRANGEBYLEX可以帮助我们实现电话号码或姓名的排序,我们以ZRANGEBYLEX为例注意:不要在分数不一致的SortSet集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确。 电话号码排序我们可以将电话号码存储到SortSet中,然后根据需要来获取号段: Copyredis> zadd phone 0 13100111100 0 13110114300 0 13132110901 (integer) 3redis> zadd phone 0 13200111100 0 13210414300 0 13252110901 (integer) 3redis> zadd phone 0 13300111100 0 13310414300 0 13352110901 (integer) 3获取所有号码: Copyredis> ZRANGEBYLEX phone - +1) "13100111100"2) "13110114300"3) "13132110901"4) "13200111100"5) "13210414300"6) "13252110901"7) "13300111100"8) "13310414300"9) "13352110901"获取132号段: Copyredis> ZRANGEBYLEX phone [132 (1331) "13200111100"2) "13210414300"3) "13252110901"获取132、133号段: Copyredis> ZRANGEBYLEX phone [132 (1341) "13200111100"2) "13210414300"3) "13252110901"4) "13300111100"5) "13310414300"6) "13352110901"姓名排序将名称存储到SortSet中: Copyredis> zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua (integer) 6获取所有人的名字: Copyredis> ZRANGEBYLEX names - +1) "Aidehua"2) "Aimini"3) "Bluetuo"4) "Gaodeng"5) "Jake"6) "Toumas"获取名字中大写字母A开头的所有人: Copyredis> ZRANGEBYLEX names [A (B1) "Aidehua"2) "Aimini"获取名字中大写字母C到Z的所有人: Copyredis> ZRANGEBYLEX names [C [Z1) "Gaodeng"2) "Jake"3) "Toumas"小结#本篇文章我们总结了Redis 有序集合对象的内部实现、常用命令以及常用的一些场景,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。那么大家在项目中对Redis有序集合对象的使用都有哪些场景呢,欢迎在评论区给我留言和分享,我会第一时间反馈!我们共同学习与进步! 参考#《Redis设计与实现》 《Redis开发与运维》 《Redis官方文档》 -----END-----#作者: 老於` 出处:https://www.cnblogs.com/hunternet/p/12717643.html
Nginx知多少系列之(六)Linux下.NET Core项目负载均衡 目录1.前言2.安装3.配置文件详解4.工作原理5.Linux下托管.NET Core项目6.Linux下.NET Core项目负载均衡7.负载均衡策略详解8.Linux下.NET Core项目Nginx+Keepalived高可用(主从模式)9.Linux下.NET Core项目Nginx+Keepalived高可用(双主模式)10.Linux下.NET Core项目LVS+Keepalived+Nginx高可用集群11.构建静态服务器12.日志分析13.优化策略14.总结 在上一篇文章我们已经讲过如何使用Nginx托管.NET Core项目,那么接下来我们就要介绍如何使用Nginx作为负载均衡。 1.什么是负载均衡? Load balancing,即负载均衡,是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。 2.有什么作用?①、解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力); ②、提供故障转移,实现高可用; ③、通过添加或减少服务器数量,提供网站伸缩性(扩展性); ⑤、安全防护;(负载均衡设备上做一些过滤,黑白名单等处理) 3.为什么使用Nginx来做负载均衡?在前一篇文章,我们只是单机部署站点,程序猿A很开心的把站点部署上线了,客户使用了一段时间也没有发现什么问题,突然有一天访问不了,程序猿A一看,哦豁,服务器挂了,让我来重启下服务器,重启之后他很开心的告诉客户,系统已经可以访问啦。同时心里庆幸在上班时间出现问题,万一半夜或者在路上,那客户不得急死咯。程序猿A就想了,这可是概率事件啊,我也不知道它什么时候发神经或者经常性的给我来故障,那我不得经常被老板拿去祭天咯。程序猿A到最后也没有解决这个故障问题,当然后面也没有发生故障,随着客户越来越多,突然某天某个点系统突然很卡甚至返回访问不了,程序猿A去看了下,发现服务器并没有故障,但是就是访问不了。这个时候程序猿A不解决问题就不行了,不解决客户就会经常访问不了,久之这个系统就没法用了,即使老板天天拿程序猿A去祭天也无济于事。 上面这个故事其实就是因为单机部署发生的单点故障,导致系统无法访问。还有就是并发鸭梨,导致系统处理能力下降甚至访问不了。那这个时候我们就把系统部署在多台服务器上,当一台挂了,另外一台也可以照常使用,而多台服务器就需要Nginx作为代理服务器,所有的请求先进入Nginx,Nginx在根据具体的规则把请求转发到具体的服务器上。 4.怎么做? 首先我们还是按照上一篇文章介绍的,部署两台.NET Core站点。但是我们要做为这两个.NET Core做一些区分,这样能更好的看出我们访问的是哪台服务器。我们按照《.NET Core项目部署到Linux(Centos7)(三)创建.NET Core API项目》,然后找到WeatherForecastController修改Get方法,增加ServerName区分具体访问的是哪一个站点。如下图 两台服务器的IP分别为192.168.157.132和192.168.157.133(这里的IP根据具体环境变化),我们修改ServerName为“.Net Core Nginx Server 1”,发布到132这台服务器上。然后修改ServerName为“.Net Core Nginx Server 2”,发布到133这台服务器上。发布完之后,我们在Postman验证下效果。《.NET Core项目部署到Linux(Centos7)(六)发布.NET Core 项目到Linux》 这样我们单独去访问这两台服务器站点都是正常的,但是这样我们是无法做负载均衡的,用户只能访问一个站点,因为只能公布一个入口,那么下面我们就部署Nginx负载均衡服务器,用它来把用户的请求转发到这两个站点的其中一个。 我们先新建一个虚拟机,然后用yum安装Nginx,《Nginx知多少系列之(二)安装》 ,安装后如下图 这一台服务器是192.168.157.134,我们按照前面介绍的为Nginx开机自启动以及开放对应的防火墙端口。 接下来我们就要做负载均衡配置了。 进入nginx目录 cd /etc/nginx 编辑nginx.conf sudo vim nginx.conf 按i进入插入模式 注释下面的内容 server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } 编辑完后按Esc,然后:wq保存退出 负载均衡需要使用upstream模块,所以我们首先需要在Http块里的全局位置定义一组服务器,我们在使用yum安装的nginx在nginx.conf里会定义了include /etc/nginx/conf.d/*.conf,这里说明了包含了conf.d文件夹下面以.conf为结尾的配置文件。因为include是定义在全局的,所以在conf配置文件里也可以定义全局的内容。 进入conf.d目录 cd /etc/nginx/conf.d 创建upstream.conf文件 sudo touch upstream.conf 编辑upstream.conf文件 sudo vim upstream.conf 按i进入插入模式 输入下面的配置内容 upstream netCoreDemo { server 192.168.157.132; server 192.168.157.133; } server { listen 80; location / { proxy_pass http://netCoreDemo; } } 按Esc,然后:wq保存退出 重启Nginx sudo nginx -s reload 配置负载均衡就是这么简单哦,当然下面我们还会介绍负载均衡的策略。不过我们先来验证下效果吧,有图有真相。 第一次访问: 第二次访问: 哦豁,看到效果了么,第一次访问是在132的站点,第二次访问是在133的站点,简单的负载均衡已经实现了。这里的策略我们没有做更改,在Nginx里默认的方式是轮询,每个请求按照时间顺序轮流分配到不同的后端服务器。 下一篇文章将会详细的讲解Nginx负载均衡的策略,目前暂不包含商业策略的讲解。 作者:江远良出处:http://www.cnblogs.com/jayjiang/
使用Maven Archetype创建Java项目模板 1.over view简而言之,Archetype是一个Maven项目模板工具包。原型被定义为一种原始的模式或模型,所有其他同类的东西都是从中产生的。当我们试图提供一个提供生成Maven项目的一致方法的系统时,这个名字就合适了。Archetype将帮助作者为用户创建Maven项目模板,并为用户提供生成这些项目模板的参数化版本的方法。 使用原型提供了一种很好的方法,可以与您的项目或组织所采用的最佳实践一致的方式快速地使开发人员受益。您可能希望在组织内部实现J2EE开发的标准化,因此您可能希望提供EJB,WAR或Web服务的原型。一旦创建了这些原型并将其部署在组织的存储库中,组织中的所有开发人员就可以使用它们。 2.do it⚠️:我们将使用springboot项目来演示如何生成一个maven archetype(原型),本文中(模板)(原型)交替使用,二者意思相同。 示例,我们有一个现成的项目,其结构如下: .├── Dockerfile├── README.md├── last-demo.iml├── mvnw├── mvnw.cmd├── pom.xml├── src ├── main │ ├── java │ │ └── com │ │ └── demo │ │ └── data │ │ ├── Application.java │ │ └── your_business_package │ │ ├── client │ │ │ └── DemoClient.java │ │ ├── constants │ │ │ └── YourBusinessConstants.java │ │ ├── enumerate │ │ │ └── DemoStatus.java │ │ ├── presistence │ │ │ ├── DemoRepository.java │ │ │ └── entity │ │ │ └── DemoDO.java │ │ ├── service │ │ │ └── DemoService.java │ │ └── web │ │ ├── dto │ │ │ └── DemoDTO.java │ │ └── rest │ │ └── DemoController.java │ └── resources │ ├── application.yml │ └── logback-spring.xml └── test ├── java │ └── com │ └── demo │ └── data │ └── ApplicationTests.java └── resources └── application.yml我们将使用maven archetype来创建以该项目为基础的模板。 2.1 生成模板文件夹执行以下maven命令: mvn archetype:create-from-project此时项目中会生成target/generated-sources/archetype文件夹,其中存放的就是我们的模板相关文件。 2.2 自定义模板探索target/generated-sources/archetype我们可以得知: generated-sources └── archetype ├── pom.xml ├── src │ ├── main │ │ └── resources │ │ ├── META-INF │ │ │ └── maven │ │ │ └── archetype-metadata.xml ##⚠️原型描述符,描述了我们原型的结构 │ │ └── archetype-resources ##⚠️经过maven转换后的项目文件包 │ └── test │ └── resources │ └── projects │ └── basic └── target ├── classes │ └── archetype-resources ├── your_project_name.jar └── test-classes └── projects └── basic 我们随机打开一个archetype-resources中的源文件,可以看到如下: 上图中我们看到的${package}占位符,这个就是maven原型插件自动处理的结果,到时候我们根据原型生成项目的时候,这些占位符就会变成我们新生成项目的相关的值。类似,maven还提供了groupId,artifactId, version等关键字。如果我们项目中有其他地方也需要这种定制化,我们可以手动进行更改。 例如我们把项目配置文件改为如下(应用名用占位符代替),目的是实现项目的名称随新建的项目变动。 接下来来分析archetype-metadata.xml,他是原型描述符号,我们可以指定那些文件进入原型里,那些文件需要排除,还能指定上面说的占位符需不需要被替换 等等。 如下为archetype-metadata.xml示例: <?xml version="1.0" encoding="UTF-8"?> <fileSet filtered="true" packaged="true" encoding="UTF-8"> <directory>src/main/java</directory> <includes> <include>**/*.java</include> </includes> </fileSet> <fileSet filtered="true" encoding="UTF-8"> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> </includes> </fileSet> <fileSet filtered="true" encoding="UTF-8"> <directory>src/main/resources</directory> <includes> <include>**/*.yml</include> </includes> </fileSet> <!--下面还有更多项--> ⚠️:fileSet属性标签指定的那些文件需要纳入原型中,我们把不需要的删掉。 ⚠️:filtered属性标签表示是否替换文件中的占位符,若为true则会替换,否则不会,所以我们如果想要占位符最后会被替换为项目相关的信息,还需要通过这个标签指定。 ⚠️:packaged属性标签指定文件是否在项目的包里面,true或false。 2.3 生成模板(原型)我们进入target/generated-sources/archetype目录,执行以下命令: mvn install此时模板将在我们本地生成。 2.4 使用模板(原型)生成新项目我们使用以下命令: mvn archetype:generate \-DarchetypeCatalog=local \-DgroupId=新建项目的groupId \-DartifactId=新建项目的artifactId \-DarchetypeGroupId=你的原型group \-DarchetypeArtifactId=你的原型项目名字-archetype \-DarchetypeVersion=你的原型版本 \-DinteractiveMode=false 之后,我们会生成新项目。项目的结构符合我们的原型结构。查看我们手动指定的application.yml 可以看到我们的占位符被我们项目的相关信息给替换了。 2.5 将模板上传至maven仓库我们进入target/generated-sources/archetype目录,打开pom.xml 添加仓库信息: <repository> <id>my-releases</id> <url>你的仓库地址</url> </repository> <snapshotRepository> <id>my-snapshots</id> <url>你的仓库地址</url> </snapshotRepository> <server> <id>my-snapshots</id> <username>对应仓库的username</username> <password>对应仓库的password</password> </server> <server> <id>my-releases</id> <username>对应仓库的username</username> <password>对应仓库的password</password> </server> 随后指定如下命令: mvn deploy随后,原型将被上传至你的mavne仓库。 3.summary本文我们介绍的maven的原型及其特性带来的好处,并且我们演示了如何生成一个原型,并且利用原型来创建一个新项目。 原文地址https://www.cnblogs.com/dongxishaonian/p/12706023.html
PYTHON工业互联网监控项目实战2—OPC OPC(OLE for Process Control)定义:指为了给工业控制系统应用程序之间的通信建立一个接口标准,在工业控制设备与控制软件之间建立统一的数据存取规范。它给工业控制领域提供了一种标准数据访问机制,将硬件与应用软件有效地分离开来,是一套与厂商无关的软件数据交换标准接口和规程,主要解决过程控制系统与其数据源的数据交换问题,可以在各个应用之间提供透明的数据访问。实际项目中“设备”就变成一个可以访问的OPC Server和它的Tag位号值,更多的详情请参考OPC基金会官网:http://opcfoundation.cn/。 上一小节我们首先通过一个简单的json格式来完成数据到UI端的传输,UI端解析Json数据,并通过JQuery渲染到div上来完成数据的显示,最后ajax轮询实现了数据的实时刷新。本小节我们把Domo进一步迭代改进,首先规范数据传输的格式,然后,实现实时读取模拟OPC Server的tag位号值。 1.1. 界面UI与Json数据结构采用面向对象的模式来定义数据传输Json格式,“设备”对象包含多个“tag”属性值。上一小节例子中,我们从后台获取的数据格式是一个Json字符串,只定义了tank4C9的相关属性。 tank4C9={ 'Status': random.randint(0,1), #设备运行状态 'OverheadFlow':random.randint(1,10) ,#'顶流量', 'ButtomsFlow': random.randint(1,10), #'低流量' 'Power': random.randint(10000,100000), #功率 } 实际项目中的监控界面会涉及到多个设备和多个监控tag位号,为了便于数据的规范管理和更新,Json数据格式构造采用面向对象的模式进行构建如下: tank4C9={ 'DeviceId': 1, 'DeviceName':'1#反应罐', 'Status': random.randint(0,1), #设备运行状态 'OverheadFlow':random.randint(1,10) ,#'顶流量', 'ButtomsFlow': random.randint(1,10), #'低流量' 'Power': random.randint(10000,100000), #功率 } Json代码就构建了一个图例反应罐主要监控数据,注意多出来的设备状态点,也就是用来体现设备是运行状态还是停机状态。另外,为了体现这个设备来自那个一个OPC Server服务,更好地体现现场设备与采集器(OPC Server)的关系,再增加一层关于采集器的Json结构python代码如下: tank4C9={ 'DeviceId': 1, 'DeviceName':'1#反应罐', 'Status': random.randint(0,1), #设备运行状态 'OverheadFlow':random.randint(1,10) ,#'顶流量', 'ButtomsFlow': random.randint(1,10), #'低流量' 'Power': random.randint(10000,100000), #功率 } Collector={ 'CollectorId': 1, 'CollectorName':'1#采集器', 'Status': 0, 'DeviceList':[tank4C9], } 从上述代码的数据关系上,我们能看出来设备“1#反应罐”属于“1#采集器”,数据采集器本身也有自己的设备运行状态位号,来标识采集设备自身是否正常运行。 1.2. 重构Collector APP代码接下来我们重构Collector APP代码,增加一个getCollectorData函数来返回连接器数据。 1.Collector APP views 增加函数getCollectorData代码如下: def getCollectorData(request): tank4C9={ 'DeviceId': 1, 'DeviceName':'1#反应罐', 'Status': random.randint(0,1), #设备运行状态 'OverheadFlow':random.randint(1,10) ,#'顶流量', 'ButtomsFlow': random.randint(1,10), #'低流量' 'Power': random.randint(10000,100000), #功率 } Collector={ 'CollectorId': 1, 'CollectorName':'1#采集器', 'Status': 0, 'DeviceList':[tank4C9], } return HttpResponse( json.dumps(Collector)); 2.修改项目urls文件urlpatterns ,发布getCollectorData path from django.urls import pathfrom Collector import views urlpatterns = [ # Uncomment the next line to enable the admin: #path('admin/', admin.site.urls) path('getTank4C9Data/', views.getTank4C9Data),path('getCollectorData/', views.getCollectorData), ] 项目调试状态我们可以通过浏览器直接访问url查看webAPI结果。http://127.0.0.1:8090/getCollectorData/ 注意:Json数据格式的变化,图中数据体现出了基于面向对象模式的层次结构“1#采集器”下面有一个包含的“设备对象列表”DeviceList属性。 1.3. 修改UI代码现在修改tank4C9.html文件里面的getData异步获取数据函数代码修改为读取上面getCollectorData函数如下: //JQuery 代码入口 $(document).ready(function(){ setInterval("getData()",1000); }); function getData() { //模拟异步从后台获得值 $.ajax({ url: "/getCollectorData/", success: function (result) { data = JSON.parse(result); tank4C9=data.DeviceList[0] $("#OverheadFlow").html(tank4C9.OverheadFlow); $("#ButtomsFlow").html(tank4C9.ButtomsFlow); $("#Power").html(tank4C9.Power); }}); } </script> 调试运行http://127.0.0.1:8090/tank4C9/ UI同样的实时自动刷新后台数据的浏览效果。 重点:代码重构的要点的功能不变的情况下,优化代码结构。 代码的结构优化,优先保证功能不变,尽量不要试图在一次迭代中引入过多的变量,这样只会导致编程工作杂乱无章的进行。Json格式代码重构完成后,接下来我们把代码调整成读取OPC Server服务的Tag点。 1.4. 从OPC Server读取Tag值现在我们重构getCollectorData函数代码,通过OPC服务读取真正的设备值,当然dome的例子是读取一个模拟OPC服务,实际项目中读取设备发布的OPC服务即可,代码如下: tank4C9={ 'DeviceId': 1, 'DeviceName':'1#反应罐', 'Status': 0, #设备运行状态 'OverheadFlow':0 ,#'顶流量', 'ButtomsFlow': 0, #'低流量' 'Power': 0, #功率 } import OpenOPC opc = OpenOPC.client() opc.connect('Matrikon.OPC.Simulation') tank4C9['OverheadFlow']= opc['Random.Int1'] tank4C9['ButtomsFlow']= opc['Random.Int2'] tank4C9['Power']= opc['Random.Int4'] opc.close() Collector={ 'CollectorId': 1, 'CollectorName':'1#采集器', 'Status': 0, 'DeviceList':[tank4C9], } return HttpResponse( json.dumps(Collector)); 现在重新调试运行并浏览监控界面http://127.0.0.1:8090/tank4C9/动态效果如下: 1.5. 小结本小节我们演示了如何在功能不变的模式下重构代码来实现功能的迭代和推进,过程中始终贯穿功能不变的前提下重构代码的结构来满足新迭代功能的要求,然后再改变代码功能读取OPC服务tag位号值。最终调整了Json的数据封装格式和实时设备数据从设备OPC Server中获取,也演示了从技术探索原型到生产原型的代码迭代过程。下一节我们将把ajax轮询演进到通过websocket来实现UI端的数据刷新,提高数据刷新的效率。 原文地址https://www.cnblogs.com/haozi0804/p/12696459.html
面试刷题37:微服务是什么?springcloud,springboot是什么? 面试中被问到为什么要使用微服务架构?springcloud的核心组件有哪些? 拿我们国家的兵种来说,如何把战争这个单体架构微服务化,就是根据适用的场景,拆分出不同的兵种(微服务) 然后每个兵种之间通过军区指挥部采用特有的通信协议连接起来(RPC) ; 每个兵种内部自治,有自己的业务,数据,部署单元(建制)对外提供打击服务(HTTP)。 微服务微服务是一种架构风格: 把单体系统拆分成各种微服务(进程集群里面),服务之间通过HTTP或者RPC协议进行通信。 服务内部是围绕某一个问题领域的业务,有自己单独的业务流程,数据存储,自动化测试,和自动化独立部署机制。 解决单体系统的难题:开发端:部分业务的修改要修改整个项目, 开发维护成本高,容易出错,不利于团队协作;运维线:部分业务的上线影响整体服务质量,运维无法精确评估系统资源的需求量; 带来的问题: 1,运维需要维护数量庞大的进程; 2,接口的业务流程拉长,一致性比较更难以控制; 3,分布式的复杂性:网络延迟,异步消息,分布式事务等; 基于敏捷项目管理和自动化部署可以应对这些问题。 springcloud整体介绍基于springboot实现的微服务架构开发工具。 提供了这些分布式问题的解决方案: springboot带来了什么?1,提供了一个开发微服务的脚手架(idea的initializer创建springcloud的微服务),减少了从0开始搭建项目的问题; 2,并非重写spring或者替代spring,主要是提供了自动化配置简化原有的样板配置 3,快速开发,提供了各种starter集成其它的组件和解决依赖管理问题 4,轻松部署,内置了web容器,轻松跟docker融合; 涵盖了项目的构建,开发,测试阶段; springboot快速使用idea的initializer创建springcloud的微服务 开发一个rest接口 开发接口的单元测试代码 例子代码点我获取! 工程结构 依赖处理1, parent处理方式 2,dependencyManagemant处理方式 运行1,java -jar x.jar 运行 正式环境 2,idea提供调试运行; 开发环境 3,maven的spring-boot:run插件运行 开发环境; springboot配置自动化配置是springboot最大的亮点。 配置的加载优先级如下: 1,命令行中的参数 ; 2, 系统环境变量中的SPRING_APPLICATION_JSON配置; 3,JNDI属性: java:comp/env 4,java的操作系统属性 System.getProperties(); 5, 操作系统的环境变量 6,jar包外部的 application-${profile}.properties 7, jar包内部的 application-${profile}.properties 8, @Configuration注解修改过的类 @PropertySource注解定义的属性 9, SpringApplication.setDefaultProperties() 多环境配置application.properties放通用配置,指定激活 dev环境 在其他的环境中提供差异化的配置,发布的时候通过命令行指定环境spring.profiles.active=prod; springboot监控微服务是的进程的数量增多,必须有一套自动化的监控运维机制来收集微服务的运行指标,进行监控和预警。 spring-boot-starter-actuator 来进行监控。 并配置开启的端点。 常见的监控端点: /health /beans /mappings 小结首先宏观上回答了为什么微服务会出现,解决了什么问题? 然后初步介绍了spring-cloud带来了什么? 接着从spring-cloud的基础出发,即springboot分析了springboot带来了什么,简单实用,配置和监控; springboot带来了什么? 以及快速使用springboot开发接口的过程; 简单介绍了 工程结构,依赖的处理方式 , 运行指令等细节; 然后基于配置,介绍了配置参数的加载顺序,多环境下的最佳实践。 最后介绍了微服务继续的自动监控和运维机制 actuator ,收集微服务的端点信息。 原文地址https://www.cnblogs.com/snidget/p/12691636.html
手把手教你分析Mysql死锁问题 前言发生死锁了,如何排查和解决呢?本文将跟你一起探讨这个问题 准备好数据环境模拟死锁案发分析死锁日志分析死锁结果环境准备数据库隔离级别: mysql> select @@tx_isolation; @@tx_isolation REPEATABLE-READ 1 row in set, 1 warning (0.00 sec)自动提交关闭: mysql> set autocommit=0;Query OK, 0 rows affected (0.00 sec) mysql> select @@autocommit; @@autocommit 0 1 row in set (0.00 sec)表结构: //id是自增主键,name是非唯一索引,balance普通字段CREATE TABLE account (id int(11) NOT NULL AUTO_INCREMENT,name varchar(255) DEFAULT NULL,balance int(11) DEFAULT NULL, PRIMARY KEY (id), KEY idx_name (name) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;表中的数据: 模拟并发开启两个终端模拟事务并发情况,执行顺序以及实验现象如下: 1)事务A执行更新操作,更新成功 mysql> update account set balance =1000 where name ='Wei';Query OK, 1 row affected (0.01 sec)2)事务B执行更新操作,更新成功 mysql> update account set balance =1000 where name ='Eason';Query OK, 1 row affected (0.01 sec)3)事务A执行插入操作,陷入阻塞~ mysql> insert into account values(null,'Jay',100); 这时候可以用select * from information_schema.innodb_locks;查看锁情况: 4)事务B执行插入操作,插入成功,同时事务A的插入由阻塞变为死锁error。 mysql> insert into account values(null,'Yan',100);Query OK, 1 row affected (0.01 sec) 锁介绍在分析死锁日志前,先做一下锁介绍,哈哈~ 主要介绍一下兼容性以及锁模式类型的锁: 共享锁与排他锁InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。 共享锁(S锁):允许持锁事务读取一行。排他锁(X锁):允许持锁事务更新或者删除一行。如果事务 T1 持有行 r 的 s 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理: T2 请求 s 锁立即被允许,结果 T1 T2 都持有 r 行的 s 锁T2 请求 x 锁不能被立即允许如果 T1 持有 r 的 x 锁,那么 T2 请求 r 的 x、s 锁都不能被立即允许,T2 必须等待T1释放 x 锁才可以,因为X锁与任何的锁都不兼容。 意向锁意向共享锁( IS 锁):事务想要获得一张表中某几行的共享锁意向排他锁( IX 锁): 事务想要获得一张表中某几行的排他锁比如:事务1在表1上加了S锁后,事务2想要更改某行记录,需要添加IX锁,由于不兼容,所以需要等待S锁释放;如果事务1在表1上加了IS锁,事务2添加的IX锁与IS锁兼容,就可以操作,这就实现了更细粒度的加锁。 InnoDB存储引擎中锁的兼容性如下表: 记录锁(Record Locks)记录锁是最简单的行锁,仅仅锁住一行。如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。会阻塞其他事务对其插入、更新、删除记录锁的事务数据(关键词:lock_mode X locks rec but not gap),记录如下: RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table test.t trx id 10078 lock_mode X locks rec but not gapRecord lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000000274f; asc 'O;; 2: len 7; hex b60000019d0110; asc ;;间隙锁(Gap Locks)间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。间隙锁只阻止其他事务插入到间隙中,他们不阻止其他事务在同一个间隙上获得间隙锁,所以 gap x lock 和 gap s lock 有相同的作用。间隙锁的事务数据(关键词:gap before rec),记录如下: RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table test2.account trx id 38049 lock_mode X locks gap before recRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 3; hex 576569; asc Wei;; 1: len 4; hex 80000002; asc ;;Next-Key LocksNext-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。插入意向锁(Insert Intention)插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,亦即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方因为插入行不冲突。事务数据类似于下面: RECORD LOCKS space id 31 page no 3 n bits 72 index PRIMARY of table test.childtrx id 8731 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000066; asc f;; 1: len 6; hex 000000002215; asc " ;; 2: len 7; hex 9000000172011c; asc r ;;...锁模式兼容矩阵(横向是已持有锁,纵向是正在请求的锁): 如何读懂死锁日志?show engine innodb status可以用show engine innodb status,查看最近一次死锁日志哈~,执行后,死锁日志如下: 2020-04-11 00:35:55 0x243c* (1) TRANSACTION:TRANSACTION 38048, ACTIVE 92 sec insertingmysql tables in use 1, locked 1LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2MySQL thread id 53, OS thread handle 2300, query id 2362 localhost ::1 root updateinsert into account values(null,'Jay',100)* (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table test2.account trx id 38048 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 3; hex 576569; asc Wei;; 1: len 4; hex 80000002; asc ;; * (2) TRANSACTION:TRANSACTION 38049, ACTIVE 72 sec inserting, thread declared inside InnoDB 5000mysql tables in use 1, locked 15 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2MySQL thread id 52, OS thread handle 9276, query id 2363 localhost ::1 root updateinsert into account values(null,'Yan',100)* (2) HOLDS THE LOCK(S):RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table test2.account trx id 38049 lock_mode X locks gap before recRecord lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 3; hex 576569; asc Wei;; 1: len 4; hex 80000002; asc ;; * (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table test2.account trx id 38049 lock_mode X insert intention waitingRecord lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; * WE ROLL BACK TRANSACTION (1)我们如何分析以上死锁日志呢? 第一部分1)找到关键词TRANSACTION,事务38048 2)查看正在执行的SQL insert into account values(null,'Jay',100)3)正在等待锁释放(WAITING FOR THIS LOCK TO BE GRANTED),插入意向排他锁(lock_mode X locks gap before rec insert intention waiting),普通索引(idx_name),物理记录(PHYSICAL RECORD),间隙区间(未知,Wei); 第二部分1)找到关键词TRANSACTION,事务38049 2)查看正在执行的SQL insert into account values(null,'Yan',100)3)持有锁(HOLDS THE LOCK),间隙锁(lock_mode X locks gap before rec),普通索引(index idx_name),物理记录(physical record),区间(未知,Wei); 4)正在等待锁释放(waiting for this lock to be granted),插入意向锁(lock_mode X insert intention waiting),普通索引上(index idx_name),物理记录(physical record),间隙区间(未知,+∞); 5)事务1回滚(we roll back transaction 1); 查看日志结果 查看日志可得: 事务A正在等待的插入意向排他锁(事务A即日志的事务1,根据insert语句来对号入座的哈),正在事务B的怀里~事务B持有间隙锁,正在等待插入意向排它锁这里面,有些朋友可能有疑惑, 事务A持有什么锁呢?日志根本看不出来。它又想拿什么样的插入意向排他锁呢?事务B拿了具体什么的间隙锁呢?它为什么也要拿插入意向锁?死锁的死循环是怎么形成的?目前日志看不出死循环构成呢?我们接下来一小节详细分析一波,一个一个问题来~ 死锁分析死锁死循环四要素 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。事务A持有什么锁呢?它又想拿什么样的插入意向排他锁呢?为了方便记录,例子用W表示Wei,J表示Jay,E表示Eason哈~ 我们先来分析事务A中update语句的加锁情况~update account set balance =1000 where name ='Wei';间隙锁: Update语句会在非唯一索引的name加上左区间的间隙锁,右区间的间隙锁(因为目前表中只有name='Wei'的一条记录,所以没有中间的间隙锁~),即(E,W) 和(W,+∞)为什么存在间隙锁?因为这是RR的数据库隔离级别,用来解决幻读问题用的~记录锁 因为name是索引,所以该update语句肯定会加上W的记录锁Next-Key锁 Next-Key锁=记录锁+间隙锁,所以该update语句就有了(E,W]的 Next-Key锁综上所述,事务A执行完update更新语句,会持有锁: Next-key Lock:(E,W]Gap Lock :(W,+∞)我们再来分析一波事务A中insert语句的加锁情况insert into account values(null,'Jay',100);间隙锁: 因为Jay(J在E和W之间),所以需要请求加(E,W)的间隙锁插入意向锁(Insert Intention) 插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即事务A需要插入意向锁(E,W)因此,事务A的update语句和insert语句执行完,它是持有了 (E,W]的 Next-Key锁,(W,+∞)的Gap锁,想拿到 (E,W)的插入意向排它锁,等待的锁跟死锁日志是对上的,哈哈~ 事务B拥有了什么间隙锁?它为什么也要拿插入意向锁?同理,我们再来分析一波事务B,update语句的加锁分析:update account set balance =1000 where name ='Eason';间隙锁: Update语句会在非唯一索引的name加上左区间的间隙锁,右区间的间隙锁(因为目前表中只有name='Eason'的一条记录,所以没有中间的间隙锁~),即(-∞,E)和(E,W)记录锁 因为name是索引,所以该update语句肯定会加上E的记录锁Next-Key锁 Next-Key锁=记录锁+间隙锁,所以该Update语句就有了(-∞,E]的 Next-Key锁综上所述,事务B执行完update更新语句,会持有锁: Next-key Lock:(-∞,E]Gap Lock :(E,W)我们再来分析一波B中insert语句的加锁情况insert into account values(null,'Yan',100);间隙锁: 因为Yan(Y在W之后),所以需要请求加(W,+∞)的间隙锁插入意向锁(Insert Intention) 插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即事务A需要插入意向锁(W,+∞)所以,事务B的update语句和insert语句执行完,它是持有了 (-∞,E]的 Next-Key锁,(E,W)的Gap锁,想拿到 (W,+∞)的间隙锁,即插入意向排它锁,加锁情况跟死锁日志也是对上的~ 死锁真相还原接下来呢,让我们一起还原死锁真相吧哈哈 事务A执行完Update Wei的语句,持有(E,W]的Next-key Lock,(W,+∞)的Gap Lock ,插入成功~事务B执行完Update Eason语句,持有(-∞,E]的 Next-Key Lock,(E,W)的Gap Lock,插入成功~事务A执行Insert Jay的语句时,因为需要(E,W)的插入意向锁,但是(E,W)在事务B怀里,所以它陷入心塞~事务B执行Insert Yan的语句时,因为需要(W,+∞) 的插入意向锁,但是(W,+∞) 在事务A怀里,所以它也陷入心塞。事务A持有(W,+∞)的Gap Lock,在等待(E,W)的插入意向锁,事务B持有(E,W)的Gap锁,在等待(W,+∞) 的插入意向锁,所以形成了死锁的闭环(Gap锁与插入意向锁会冲突的,可以看回锁介绍的锁模式兼容矩阵哈)事务A,B形成了死锁闭环后,因为Innodb的底层机制,它会让其中一个事务让出资源,另外的事务执行成功,这就是为什么你最后看到事务B插入成功了,但是事务A的插入显示了Deadlock found ~总结最后,遇到死锁问题,我们应该怎么分析呢? 模拟死锁场景show engine innodb status;查看死锁日志找出死锁SQLSQL加锁分析,这个可以去官网看哈分析死锁日志(持有什么锁,等待什么锁)熟悉锁模式兼容矩阵,InnoDB存储引擎中锁的兼容性矩阵。原文地址https://www.cnblogs.com/jay-huaxiao/p/12685287.html
2022年02月
2022年01月